Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unable to handle bound getter/setter errors #14892

Closed
colecrouter opened this issue Jan 5, 2025 · 6 comments
Closed

Unable to handle bound getter/setter errors #14892

colecrouter opened this issue Jan 5, 2025 · 6 comments

Comments

@colecrouter
Copy link

Describe the bug

I noticed when you throw an error via a setter, then bind an input to it, that error is not treated as a UI/rendering/effect error; using +error.svelte or <svelte:boundary> to try and catch the error does not result in anything happening. Instead, the error is logged to the console as "uncaught", and everything continues like nothing went wrong.

—except it doesn't; some related event handlers/effects silently fail.

While I can appreciate that a getter/setter error isn't strictly UI/rendering/effect related, leaving the app in a half-working/UB state is a poor solution.

Is there another pattern for an issue like this? What ought someone do to handle these errors gracefully?

Reproduction

https://svelte.dev/playground/88805747a51c4ae981d01fff06573370?version=5.16.1

Enter a number into the box, notice how the UI updates + the side effect in the console.

Now delete the number. The error should appear in the console. Subsequent inputs will reflect in the UI, but notice how the "side effect" no longer runs (until the page is reloaded).

Lastly, none of this is caught by the <svelte:boundary>.

Logs

No response

System Info

System:
  OS: Windows 11 10.0.26100
  CPU: (20) x64 12th Gen Intel(R) Core(TM) i7-12700K
  Memory: 6.27 GB / 15.79 GB
Binaries:
  Node: 22.9.0 - C:\Program Files\nodejs\node.EXE
  Yarn: 1.22.19 - ~\AppData\Roaming\npm\yarn.CMD
  npm: 10.8.3 - C:\Program Files\nodejs\npm.CMD
Browsers:
  Edge: Chromium (130.0.2849.46)
  Internet Explorer: 11.0.26100.1882
npmPackages:
  svelte: ^5.16.1 => 5.16.1

Severity

annoyance

@Leonidaz
Copy link

Leonidaz commented Jan 5, 2025

according to the documentation, error boundaries do not work for event handlers. oninput is obviously is an event handler but even bind:value (with a signal or with a getter and setter ) is also done through an input event.

from https://svelte.dev/docs/svelte/svelte-boundary

Errors occurring outside the rendering process (for example, in event handlers) are not caught by error boundaries.

typically, any such errors that fall outside of the svelte:boundary, one would handle via a try/catch block but if the provided example indeed your use case, you would display an error to the user. Either use form validation provided attributes https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Forms/Form_validation or use custom errors via setCustomValidity via bind:{getter, setter}:

Example of validation:

Or, you can pass in the input element (bind:this={input}) to your class instance and do the same in your value setter, while still using bind:value

Or, don't need to use bind: and just use the oninput event to perform validation

@colecrouter
Copy link
Author

colecrouter commented Jan 5, 2025

Thanks for the info @Leonidaz. I hadn't explored using setCustomValidity for this purpose, nor have I gotten familiar with the recent syntax changes. It sounds like that could be a valid alternative.

Perhaps one could write a bind-able <Input> component, which implements its own getter & setter, while wrapping the actual assignment logic in a try/catch inside oninput. It sounds like a bit of an awkward solution, but I will definitely try this.

I think you might misunderstand me on the event handlers. In the example above, the event handler is not throwing an error, rather the setter is. Once the setter throws an error, the (unrelated) event handler ceases to fire, until the page is reloaded, but the signal still appears to work fine.

If I had to ask a follow-up question, it would be "why?" From a DX standpoint, the runtime difference between:

function sideEffect() {
  console.log("side effect");
  throw "banana";
}

—and

function sideEffect() {
  console.log("side effect");
}
throw "banana";

—seems marginal, but (in practice) depends entirely on where sideEffect() is called. In the same vein, if I await a promise at the component level, that is considered "UI level". So if I create a promise, then resolve the promise inside of an event handler, and await it in UI, that should trigger the boundary... right? It seems a little awkward, and has me doubting my understanding. Maybe I'm missing some important info.

@Leonidaz
Copy link

Leonidaz commented Jan 5, 2025

I think you might misunderstand me on the event handlers. In the example above, the event handler is not throwing an error, rather the setter is.

The setter is fired because of bind:value and bind:value is implemented via an input event, looks like directly on the input element vs delegated, so the setter is a part of the event handling - input event calls the setter by setting the value inside the svelte-created input event.

Your sideEffect() function called from your oninput event still fires because oninput is a delegated event, on the document. Svelte delegates events to the document and also to the parent component in case bubbling is cancelled, e.g. stopPropagation() is called.

The reason why the oninput (delegated) event still fires is because that's how browsers work, even if there is an error in a handler the delegated handler up the dom tree still fires. The events are asynchronous.

Once the setter throws an error, the (unrelated) event handler ceases to fire, until the page is reloaded, but the signal still appears to work fine.

In the playground, sideEffect(), upon repeatedly changing the input's value in the UI, still fires from your oninput event after an error thrown in the setter without reloading the browser. So, not sure what you mean here, some code that wasn't provided?

Perhaps one could write a bind-able component, which implements its own getter & setter, while wrapping the actual assignment logic in a try/catch inside oninput. It sounds like a bit of an awkward solution, but I will definitely try this.

I'm not sure what errors you expect from the input, unless your code generates them.

If I had to ask a follow-up question, it would be "why?" From a DX standpoint, the runtime difference between:

function sideEffect() {
  console.log("side effect");
  throw "banana";
}

—and

function sideEffect() {
  console.log("side effect");
}
throw "banana";

Not sure what you mean here, it would be helpful to have an an actual example vs a hypothetical.

If I await a promise at the component level, that is considered "UI level". So if I create a promise, then resolve the promise inside of an event handler, and await it in UI, that should trigger the boundary... right? It seems a little awkward, and has me doubting my understanding. Maybe I'm missing some important info.

{#await} block can have a {:catch} block. Boundaries deal with errors occurring during rendering (template effects) or in user effects since they can only capture errors in the synchronous code, anything async (events, setTimeout, async await) would not be able to be captured. So, the boundary would not work in this case. So, either use a catch block or try / catch in your async code. In general, the boundaries were created to be able to handle rendering / reactivity errors that were impossible to catch otherwise. All other errors if they could happen can be handled by code, caught by using try / catch. To capture user effects (outside rendering), the error boundary has to be set on the parent component.

So, technically you can create a way to capture errors with boundaries but it's an ugly solution that you should never use because it's easily done with catching errors using traditional methods.

using $effect to throw errors and capture with a <svelte:boundary>

The other thing to remember with svelte:boundaries is that it destroys the component / elements and they have to be recreated again: via a snippet (not safe as it could lead to infinite loops) or via a reset button.

@colecrouter
Copy link
Author

That's a great explanation, thanks.

Not sure what you mean here, it would helpful to have an an actual example vs a hypothetical.

I think you answered my question, which was that logical errors (unless directly applicable to signals) don't trigger rendering errors, which was a misunderstanding on my part.

So, technically you can create a way to capture errors with boundaries but it's an ugly solution that you should never use because it's easily done with catching errors using traditional methods.

I took a crack at solving my problem here. It's not as clean, but it keeps the error handling inside the component, rather than the class. Please let me know if this is along the lines of what you were initially suggesting.

To capture user effects (outside rendering), the error boundary has to be set on the parent component.

I wanted to clarify what you meant by this. I tried this here, but I wasn't able to get it to work as expected.

In the playground, sideEffect(), upon repeatedly changing the input's value in the UI

Maybe we aren't seeing the same thing.

Recording.2025-01-05.133206.mp4

What I am seeing is that oninput stops working after the first setter error, but the reactivity still works. Apologies if I'm misunderstanding.

@Leonidaz
Copy link

Leonidaz commented Jan 5, 2025

In the playground, sideEffect(), upon repeatedly changing the input's value in the UI

Maybe we aren't seeing the same thing.

Recording.2025-01-05.133206.mp4
What I am seeing is that oninput stops working after the first setter error, but the reactivity still works. Apologies if I'm misunderstanding.

Ah, the playground console sucks, I only use the browser's (chrome) dev tools and if you look there, your handler keeps firing.

So, technically you can create a way to capture errors with boundaries but it's an ugly solution that you should never use because it's easily done with catching errors using traditional methods.

I took a crack at solving my problem here. It's not as clean, but it keeps the error handling inside the component, rather than the class. Please let me know if this is along the lines of what you were initially suggesting.

This example works although the svelte:boundary doesn't do anything as far as the validation errors. I don't know your requirements but if you need to use a class for validation and throw errors then this seems like a valid approach. I would personally just collect errors into something reactive and then display them as error messages below inputs or by using setCustomValidity to get the native browser experience.

To capture user effects (outside rendering), the error boundary has to be set on the parent component.

I wanted to clarify what you meant by this. I tried this here, but I wasn't able to get it to work as expected.

I really don't recommend it for your use case, as mentioned. I only mentioned it because you were trying to make the error boundary work. But, the only way it would work in your use case if you threw an error inside an effect. Here's a repl using your example

notice how the component disappears after the svelte boundary catches it, as you need fallback content and/or reset button.

@colecrouter
Copy link
Author

Ah, the playground console sucks,

That's one mystery solved.

I would personally just collect errors into something reactive and then display them as error messages below inputs or by using setCustomValidity to get the native browser experience.

For sure, I think input validation + either of our solutions works better for my specific case (thanks again).

As error handling has seemed mostly unecessary in the Svelte ecosystem, I was aware that my use case is probably an anti-pattern. I'm just exploring every avenue to better my understanding of how errors are bubbling here, and of course for anyone else who finds themselves recreating this wheel.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants