Skip to content

Commit 942cda7

Browse files
committed
handle errors in the correct boundary
1 parent 3ad0519 commit 942cda7

File tree

9 files changed

+82
-167
lines changed

9 files changed

+82
-167
lines changed

documentation/docs/98-reference/.generated/client-errors.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,23 @@ let odd = $derived(!even);
158158
```
159159

160160
If side-effects are unavoidable, use [`$effect`]($effect) instead.
161+
162+
### svelte_boundary_reset_onerror
163+
164+
```
165+
A `<svelte:boundary>` `reset` function cannot be called while an error is still being handled
166+
```
167+
168+
If a [`<svelte:boundary>`](https://svelte.dev/docs/svelte/svelte-boundary) has an `onerror` function, it must not call the provided `reset` function synchronously since the boundary is still in a broken state. Typically, `reset()` is called later, once the error has been resolved.
169+
170+
If it's possible to resolve the error inside the `onerror` callback, you must at least wait for the boundary to settle before calling `reset()`, for example using [`tick`](https://svelte.dev/docs/svelte/lifecycle-hooks#tick):
171+
172+
```svelte
173+
<svelte:boundary onerror={async (error, reset) => {
174+
fixTheError();
175+
+++await tick();+++
176+
reset();
177+
}}>
178+
179+
</svelte:boundary>
180+
```

documentation/docs/98-reference/.generated/client-warnings.md

Lines changed: 0 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -200,74 +200,6 @@ Consider the following code:
200200
201201
To fix it, either create callback props to communicate changes, or mark `person` as [`$bindable`]($bindable).
202202
203-
### reset_misuse
204-
205-
```
206-
reset() was invoked and the `<svelte:boundary>` template threw during flush. Calling `reset` inside the `onerror` handler while the app state is still broken can cause the fresh template to crash during its first render; the error bypassed the <svelte:boundary> to avoid an infinite loop `error``reset``error`
207-
```
208-
209-
When you call `reset()` Svelte tears down the template inside `<svelte:boundary>` and renders a fresh one. If the same bad state that caused the first error in the first place is still present, that fresh mount crashes immediately. To break a potential `error → reset → error` loop, Svelte lets such render-time errors bubble past the boundary.
210-
211-
Sometimes this happens because you might have called `reset` before the error was thrown (perhaps in the `onclick` handler of the button that will then trigger the error) or inside the `onerror` handler.
212-
213-
`reset()` should preferably be called **after** the boundary has entered its error state. A common pattern is to call it from a "Try again" button in the fallback UI.
214-
215-
If you need to call `reset` inside the `onerror` handler, ensure you fix the broken state first, *then* invoke `reset()`.
216-
217-
The examples below show do's and don'ts:
218-
219-
```svelte
220-
<!-- ❌ Don't call reset before errors occur -->
221-
<button onclick={() => {
222-
showComponent = true;
223-
if (reset) reset(); // Called before knowing if error will occur
224-
}}>
225-
Update
226-
</button>
227-
228-
<svelte:boundary>
229-
{#if showComponent}
230-
<!-- ... -->
231-
{/if}
232-
</svelte:boundary>
233-
```
234-
235-
```svelte
236-
<!-- ❌ Don't call reset without fixing the problematic state -->
237-
<svelte:boundary onerror={() => {
238-
// Fix the problematic state first
239-
reset(); // This will cause the error to be thrown again and bypass the boundary
240-
}}>
241-
<!-- ... -->
242-
</svelte:boundary>
243-
```
244-
245-
```svelte
246-
<!-- ✅ Call reset from error UI -->
247-
<svelte:boundary>
248-
<!-- ... -->
249-
250-
{#snippet failed(error)}
251-
<button onclick={() => {
252-
// Fix the problematic state first
253-
selectedItem = null;
254-
userInput = '';
255-
reset(); // Now safe to retry
256-
}}>Try Again</button>
257-
{/snippet}
258-
</svelte:boundary>
259-
```
260-
261-
```svelte
262-
<!-- ✅ Or fix the problematic state first and call reset in the onerror for immediate recovery -->
263-
<svelte:boundary onerror={() => {
264-
componentState = initialComponentState; // Fix/reset the problematic state first
265-
reset(); // Now the regular template will show without errors
266-
}}>
267-
<!-- ... -->
268-
</svelte:boundary>
269-
```
270-
271203
### select_multiple_invalid_value
272204
273205
```

packages/svelte/messages/client-errors/errors.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,21 @@ let odd = $derived(!even);
114114
```
115115

116116
If side-effects are unavoidable, use [`$effect`]($effect) instead.
117+
118+
## svelte_boundary_reset_onerror
119+
120+
> A `<svelte:boundary>` `reset` function cannot be called while an error is still being handled
121+
122+
If a [`<svelte:boundary>`](https://svelte.dev/docs/svelte/svelte-boundary) has an `onerror` function, it must not call the provided `reset` function synchronously since the boundary is still in a broken state. Typically, `reset()` is called later, once the error has been resolved.
123+
124+
If it's possible to resolve the error inside the `onerror` callback, you must at least wait for the boundary to settle before calling `reset()`, for example using [`tick`](https://svelte.dev/docs/svelte/lifecycle-hooks#tick):
125+
126+
```svelte
127+
<svelte:boundary onerror={async (error, reset) => {
128+
fixTheError();
129+
+++await tick();+++
130+
reset();
131+
}}>
132+
133+
</svelte:boundary>
134+
```

packages/svelte/messages/client-warnings/warnings.md

Lines changed: 0 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -168,72 +168,6 @@ Consider the following code:
168168
169169
To fix it, either create callback props to communicate changes, or mark `person` as [`$bindable`]($bindable).
170170
171-
## reset_misuse
172-
173-
> reset() was invoked and the `<svelte:boundary>` template threw during flush. Calling `reset` inside the `onerror` handler while the app state is still broken can cause the fresh template to crash during its first render; the error bypassed the <svelte:boundary> to avoid an infinite loop `error``reset``error`
174-
175-
When you call `reset()` Svelte tears down the template inside `<svelte:boundary>` and renders a fresh one. If the same bad state that caused the first error in the first place is still present, that fresh mount crashes immediately. To break a potential `error → reset → error` loop, Svelte lets such render-time errors bubble past the boundary.
176-
177-
Sometimes this happens because you might have called `reset` before the error was thrown (perhaps in the `onclick` handler of the button that will then trigger the error) or inside the `onerror` handler.
178-
179-
`reset()` should preferably be called **after** the boundary has entered its error state. A common pattern is to call it from a "Try again" button in the fallback UI.
180-
181-
If you need to call `reset` inside the `onerror` handler, ensure you fix the broken state first, *then* invoke `reset()`.
182-
183-
The examples below show do's and don'ts:
184-
185-
```svelte
186-
<!-- ❌ Don't call reset before errors occur -->
187-
<button onclick={() => {
188-
showComponent = true;
189-
if (reset) reset(); // Called before knowing if error will occur
190-
}}>
191-
Update
192-
</button>
193-
194-
<svelte:boundary>
195-
{#if showComponent}
196-
<!-- ... -->
197-
{/if}
198-
</svelte:boundary>
199-
```
200-
201-
```svelte
202-
<!-- ❌ Don't call reset without fixing the problematic state -->
203-
<svelte:boundary onerror={() => {
204-
// Fix the problematic state first
205-
reset(); // This will cause the error to be thrown again and bypass the boundary
206-
}}>
207-
<!-- ... -->
208-
</svelte:boundary>
209-
```
210-
211-
```svelte
212-
<!-- ✅ Call reset from error UI -->
213-
<svelte:boundary>
214-
<!-- ... -->
215-
216-
{#snippet failed(error)}
217-
<button onclick={() => {
218-
// Fix the problematic state first
219-
selectedItem = null;
220-
userInput = '';
221-
reset(); // Now safe to retry
222-
}}>Try Again</button>
223-
{/snippet}
224-
</svelte:boundary>
225-
```
226-
227-
```svelte
228-
<!-- ✅ Or fix the problematic state first and call reset in the onerror for immediate recovery -->
229-
<svelte:boundary onerror={() => {
230-
componentState = initialComponentState; // Fix/reset the problematic state first
231-
reset(); // Now the regular template will show without errors
232-
}}>
233-
<!-- ... -->
234-
</svelte:boundary>
235-
```
236-
237171
## select_multiple_invalid_value
238172
239173
> The `value` property of a `<select multiple>` element should be an array, but it received a non-array value. The selection will be kept as is.

packages/svelte/src/internal/client/dom/blocks/boundary.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/** @import { Effect, TemplateNode, } from '#client' */
22

3-
import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '#client/constants';
3+
import { BOUNDARY_EFFECT, EFFECT_RAN, EFFECT_TRANSPARENT } from '#client/constants';
44
import { component_context, set_component_context } from '../../context.js';
55
import { handle_error, invoke_error_boundary } from '../../error-handling.js';
66
import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
@@ -20,6 +20,7 @@ import {
2020
} from '../hydration.js';
2121
import { queue_micro_task } from '../task.js';
2222
import * as w from '../../warnings.js';
23+
import * as e from '../../errors.js';
2324

2425
/**
2526
* @param {Effect} boundary
@@ -96,8 +97,7 @@ export function boundary(node, props, boundary_fn) {
9697
did_reset = true;
9798

9899
if (calling_on_error) {
99-
w.reset_misuse();
100-
throw error;
100+
e.svelte_boundary_reset_onerror();
101101
}
102102

103103
pause_effect(boundary_effect);
@@ -115,6 +115,12 @@ export function boundary(node, props, boundary_fn) {
115115
calling_on_error = true;
116116
onerror?.(error, reset);
117117
calling_on_error = false;
118+
} catch (error) {
119+
if ((boundary.f & EFFECT_RAN) !== 0) {
120+
invoke_error_boundary(error, /** @type {Effect} */ (boundary.parent));
121+
} else {
122+
throw error;
123+
}
118124
} finally {
119125
set_active_reaction(previous_reaction);
120126
}

packages/svelte/src/internal/client/error-handling.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,13 @@ export function invoke_error_boundary(error, effect) {
4242
// @ts-expect-error
4343
effect.fn(error);
4444
return;
45-
} catch {}
45+
} catch (e) {
46+
if (DEV && e instanceof Error) {
47+
adjust_error(e, effect);
48+
}
49+
50+
error = e;
51+
}
4652
}
4753

4854
effect = effect.parent;

packages/svelte/src/internal/client/errors.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,4 +319,19 @@ export function state_unsafe_mutation() {
319319
} else {
320320
throw new Error(`https://svelte.dev/e/state_unsafe_mutation`);
321321
}
322+
}
323+
324+
/**
325+
* A `<svelte:boundary>` `reset` function cannot be called while an error is still being handled
326+
* @returns {never}
327+
*/
328+
export function svelte_boundary_reset_onerror() {
329+
if (DEV) {
330+
const error = new Error(`svelte_boundary_reset_onerror\nA \`<svelte:boundary>\` \`reset\` function cannot be called while an error is still being handled\nhttps://svelte.dev/e/svelte_boundary_reset_onerror`);
331+
332+
error.name = 'Svelte error';
333+
throw error;
334+
} else {
335+
throw new Error(`https://svelte.dev/e/svelte_boundary_reset_onerror`);
336+
}
322337
}

packages/svelte/src/internal/client/warnings.js

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,6 @@ export function console_log_state(method) {
4343
}
4444
}
4545

46-
/**
47-
* A `<svelte:boundary>` `reset` function only resets the boundary the first time it is called
48-
*/
49-
export function svelte_boundary_reset_noop() {
50-
if (DEV) {
51-
console.warn(`%c[svelte] svelte_boundary_reset_noop\n%cA \`<svelte:boundary>\` \`reset\` function only resets the boundary the first time it is called\nhttps://svelte.dev/e/svelte_boundary_reset_noop`, bold, normal);
52-
} else {
53-
console.warn(`https://svelte.dev/e/svelte_boundary_reset_noop`);
54-
}
55-
}
56-
5746
/**
5847
* %handler% should be a function. Did you mean to %suggestion%?
5948
* @param {string} handler
@@ -169,17 +158,6 @@ export function ownership_invalid_mutation(name, location, prop, parent) {
169158
}
170159
}
171160

172-
/**
173-
* reset() was invoked and the `<svelte:boundary>` template threw during flush. Calling `reset` inside the `onerror` handler while the app state is still broken can cause the fresh template to crash during its first render; the error bypassed the <svelte:boundary> to avoid an infinite loop `error` → `reset` → `error`
174-
*/
175-
export function reset_misuse() {
176-
if (DEV) {
177-
console.warn(`%c[svelte] reset_misuse\n%creset() was invoked and the \`<svelte:boundary>\` template threw during flush. Calling \`reset\` inside the \`onerror\` handler while the app state is still broken can cause the fresh template to crash during its first render; the error bypassed the <svelte:boundary> to avoid an infinite loop \`error\` → \`reset\` → \`error\`\nhttps://svelte.dev/e/reset_misuse`, bold, normal);
178-
} else {
179-
console.warn(`https://svelte.dev/e/reset_misuse`);
180-
}
181-
}
182-
183161
/**
184162
* The `value` property of a `<select multiple>` element should be an array, but it received a non-array value. The selection will be kept as is.
185163
*/
@@ -203,6 +181,17 @@ export function state_proxy_equality_mismatch(operator) {
203181
}
204182
}
205183

184+
/**
185+
* A `<svelte:boundary>` `reset` function only resets the boundary the first time it is called
186+
*/
187+
export function svelte_boundary_reset_noop() {
188+
if (DEV) {
189+
console.warn(`%c[svelte] svelte_boundary_reset_noop\n%cA \`<svelte:boundary>\` \`reset\` function only resets the boundary the first time it is called\nhttps://svelte.dev/e/svelte_boundary_reset_noop`, bold, normal);
190+
} else {
191+
console.warn(`https://svelte.dev/e/svelte_boundary_reset_noop`);
192+
}
193+
}
194+
206195
/**
207196
* The `slide` transition does not work correctly for elements with `display: %value%`
208197
* @param {string} value

packages/svelte/tests/runtime-runes/samples/error-boundary-reset-onerror/_config.js

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,12 @@ import { flushSync } from 'svelte';
22
import { test } from '../../test';
33

44
export default test({
5-
test({ assert, target, warnings }) {
5+
test({ assert, target }) {
66
const btn = target.querySelector('button');
77

88
btn?.click();
99

10-
assert.throws(() => {
11-
flushSync();
12-
}, 'error on template render');
13-
14-
// Check that the warning is being showed to the user
15-
assert.include(warnings[0], 'reset() was invoked');
10+
assert.throws(flushSync, 'svelte_boundary_reset_onerror');
1611

1712
// boundary content empty; only button remains
1813
assert.htmlEqual(target.innerHTML, `<button>trigger throw</button>`);

0 commit comments

Comments
 (0)