From 997ccefea96820934e0cf53d07f079635fd29922 Mon Sep 17 00:00:00 2001 From: raythurnvoid <53383860+raythurnvoid@users.noreply.github.com> Date: Sun, 15 Jun 2025 17:32:14 +0100 Subject: [PATCH 01/11] Add a warning when the misuse of `reset` in an `error:boundary` causes an error to be thrown when flushing the boundary content --- .../.generated/client-warnings.md | 68 +++++++++++++++++++ .../messages/client-warnings/warnings.md | 66 ++++++++++++++++++ .../internal/client/dom/blocks/boundary.js | 9 ++- .../svelte/src/internal/client/warnings.js | 11 +++ 4 files changed, 153 insertions(+), 1 deletion(-) diff --git a/documentation/docs/98-reference/.generated/client-warnings.md b/documentation/docs/98-reference/.generated/client-warnings.md index fe90b0db3815..e11d73e819f4 100644 --- a/documentation/docs/98-reference/.generated/client-warnings.md +++ b/documentation/docs/98-reference/.generated/client-warnings.md @@ -200,6 +200,74 @@ Consider the following code: To fix it, either create callback props to communicate changes, or mark `person` as [`$bindable`]($bindable). +### reset_misuse + +``` +reset() was invoked and the `` 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 to avoid an infinite loop `error` → `reset` → `error` +``` + +When you call `reset()` Svelte tears down the template inside `` 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. + +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. + +`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. + +If you need to call `reset` inside the `onerror` handler, ensure you fix the broken state first, *then* invoke `reset()`. + +The examples below show do's and don'ts: + +```svelte + + + + + {#if showComponent} + + {/if} + +``` + +```svelte + + { + // Fix the problematic state first + reset(); // This will cause the error to be thrown again and bypass the boundary +}}> + + +``` + +```svelte + + + + + {#snippet failed(error)} + + {/snippet} + +``` + +```svelte + + { + componentState = initialComponentState; // Fix/reset the problematic state first + reset(); // Now the regular template will show without errors +}}> + + +``` + ### select_multiple_invalid_value ``` diff --git a/packages/svelte/messages/client-warnings/warnings.md b/packages/svelte/messages/client-warnings/warnings.md index f1901271d11c..e161fe4bd909 100644 --- a/packages/svelte/messages/client-warnings/warnings.md +++ b/packages/svelte/messages/client-warnings/warnings.md @@ -168,6 +168,72 @@ Consider the following code: To fix it, either create callback props to communicate changes, or mark `person` as [`$bindable`]($bindable). +## reset_misuse + +> reset() was invoked and the `` 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 to avoid an infinite loop `error` → `reset` → `error` + +When you call `reset()` Svelte tears down the template inside `` 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. + +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. + +`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. + +If you need to call `reset` inside the `onerror` handler, ensure you fix the broken state first, *then* invoke `reset()`. + +The examples below show do's and don'ts: + +```svelte + + + + + {#if showComponent} + + {/if} + +``` + +```svelte + + { + // Fix the problematic state first + reset(); // This will cause the error to be thrown again and bypass the boundary +}}> + + +``` + +```svelte + + + + + {#snippet failed(error)} + + {/snippet} + +``` + +```svelte + + { + componentState = initialComponentState; // Fix/reset the problematic state first + reset(); // Now the regular template will show without errors +}}> + + +``` + ## select_multiple_invalid_value > The `value` property of a `` element should be an array, but it received a non-array value. The selection will be kept as is. */ From 625530168f1b165d08206a2f322e4ad68d45f5c4 Mon Sep 17 00:00:00 2001 From: raythurnvoid <53383860+raythurnvoid@users.noreply.github.com> Date: Sun, 15 Jun 2025 20:01:21 +0100 Subject: [PATCH 02/11] Prevent uncaught errors to make test fails when they are expected and are fired during template effects flush --- packages/svelte/tests/runtime-legacy/shared.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index 11ea9f6dda89..b2b850901c34 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -450,10 +450,11 @@ async function run_test_variant( 'Expected component to unmount and leave nothing behind after it was destroyed' ); - // TODO: This seems useless, unhandledRejection is only triggered on the next task - // by which time the test has already finished and the next test resets it to null above + // uncaught errors like during template effects flush if (unhandled_rejection) { - throw unhandled_rejection; // eslint-disable-line no-unsafe-finally + if (!config.expect_unhandled_rejections) { + throw unhandled_rejection; // eslint-disable-line no-unsafe-finally + } } } } From 0554c512d9821429c5ccbd029ae49bb36b9cd4c4 Mon Sep 17 00:00:00 2001 From: raythurnvoid <53383860+raythurnvoid@users.noreply.github.com> Date: Sun, 15 Jun 2025 20:01:47 +0100 Subject: [PATCH 03/11] Add tests --- .../error-boundary-reset-onerror/_config.js | 20 +++++++ .../error-boundary-reset-onerror/main.svelte | 17 ++++++ .../error-boundary-reset-premature/_config.js | 58 +++++++++++++++++++ .../main.svelte | 26 +++++++++ 4 files changed, 121 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-reset-onerror/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-reset-onerror/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-reset-premature/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-reset-premature/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-onerror/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-onerror/_config.js new file mode 100644 index 000000000000..2a18790c611e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-onerror/_config.js @@ -0,0 +1,20 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target, warnings }) { + const btn = target.querySelector('button'); + + btn?.click(); + + assert.throws(() => { + flushSync(); + }, 'error on template render'); + + // Check that the warning is being showed to the user + assert.include(warnings[0], 'reset() was invoked'); + + // boundary content empty; only button remains + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-onerror/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-onerror/main.svelte new file mode 100644 index 000000000000..f91048a9e778 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-onerror/main.svelte @@ -0,0 +1,17 @@ + + + reset()}> + {must_throw ? throw_error() : 'normal content'} + + {#snippet failed()} +
err
+ {/snippet} +
+ + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-premature/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-premature/_config.js new file mode 100644 index 000000000000..048413ad7b81 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-premature/_config.js @@ -0,0 +1,58 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + normal content + + `, + + expect_unhandled_rejections: true, + + async test({ assert, target, warnings, window }) { + // @ts-expect-error + const __expected_error = (window.__expected_error = { v: false }); + + window.addEventListener('error', (e) => { + // @ts-expect-error when in hydrate mode we can't access variables in the scope + const __expected_error = window.__expected_error; + + if (__expected_error.v) { + assert.include(e.error.message, 'error on template render'); + } else { + assert.fail('Error was not expected: ' + e.error.message); + } + e.preventDefault(); + }); + + const btn = target.querySelector('button'); + + // 1st click — error caught, fallback visible + btn?.click(); + flushSync(); + assert.htmlEqual(target.innerHTML, `
err
`); + + // 2nd click — reset succeeds, normal render + btn?.click(); + flushSync(); + assert.htmlEqual( + target.innerHTML, + ` + normal content + + ` + ); + + // 3rd click — mount-time crash escapes, boundary empty + __expected_error.v = true; + btn?.click(); + flushSync(); + __expected_error.v = false; + + // Check that the warning is being showed to the user + assert.include(warnings[0], 'reset() was invoked'); + + // boundary content empty; only button remains + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-premature/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-premature/main.svelte new file mode 100644 index 000000000000..c1462eaf09c2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-premature/main.svelte @@ -0,0 +1,26 @@ + + + + (reset = fn)}> + {must_throw ? throw_error() : 'normal content'} + + {#snippet failed()} +
err
+ {/snippet} +
+
+ + From 28b0310e359467c792931360ef74b7bf7385f895 Mon Sep 17 00:00:00 2001 From: raythurnvoid <53383860+raythurnvoid@users.noreply.github.com> Date: Sun, 15 Jun 2025 20:02:15 +0100 Subject: [PATCH 04/11] Add changeset --- .changeset/new-dogs-obey.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/new-dogs-obey.md diff --git a/.changeset/new-dogs-obey.md b/.changeset/new-dogs-obey.md new file mode 100644 index 000000000000..088d7a946236 --- /dev/null +++ b/.changeset/new-dogs-obey.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +Add a warning when the misuse of `reset` in an `error:boundary` causes an error to be thrown when re-mounting the boundary content From 0ebb878166bbe0e354177ae93fe730524956d9a7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 18 Jun 2025 08:41:47 -0400 Subject: [PATCH 05/11] reset should just be a noop after the first call --- .../.generated/client-warnings.md | 26 +++++++++ .../messages/client-warnings/warnings.md | 24 +++++++++ .../internal/client/dom/blocks/boundary.js | 9 ++++ .../svelte/src/internal/client/warnings.js | 11 ++++ .../error-boundary-reset-premature/_config.js | 54 +++++-------------- 5 files changed, 82 insertions(+), 42 deletions(-) diff --git a/documentation/docs/98-reference/.generated/client-warnings.md b/documentation/docs/98-reference/.generated/client-warnings.md index e11d73e819f4..948f4b9b2f97 100644 --- a/documentation/docs/98-reference/.generated/client-warnings.md +++ b/documentation/docs/98-reference/.generated/client-warnings.md @@ -300,6 +300,32 @@ Reactive `$state(...)` proxies and the values they proxy have different identiti To resolve this, ensure you're comparing values where both values were created with `$state(...)`, or neither were. Note that `$state.raw(...)` will _not_ create a state proxy. +### svelte_boundary_reset_noop + +``` +A `` `reset` function only resets the boundary the first time it is called +``` + +When an error occurs while rendering the contents of a [``](https://svelte.dev/docs/svelte/svelte-boundary), the `onerror` handler is called with the error plus a `reset` function that attempts to re-render the contents. + +This `reset` function should only be called once. After that, it has no effect — in a case like this, where a reference to `reset` is stored outside the boundary, clicking the button while `` is rendered will _not_ cause the contents to be rendered again. + +```svelte + + + + + (reset = r)}> + - - - - {#if showComponent} - - {/if} - -``` - -```svelte - - { - // Fix the problematic state first - reset(); // This will cause the error to be thrown again and bypass the boundary -}}> - - -``` - -```svelte - - - - - {#snippet failed(error)} - - {/snippet} - -``` - -```svelte - - { - componentState = initialComponentState; // Fix/reset the problematic state first - reset(); // Now the regular template will show without errors -}}> - - -``` - ### select_multiple_invalid_value ``` diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index c4e68f8fee80..32c4e620e320 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -114,3 +114,21 @@ let odd = $derived(!even); ``` If side-effects are unavoidable, use [`$effect`]($effect) instead. + +## svelte_boundary_reset_onerror + +> A `` `reset` function cannot be called while an error is still being handled + +If a [``](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. + +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): + +```svelte + { + fixTheError(); + +++await tick();+++ + reset(); +}}> + + +``` diff --git a/packages/svelte/messages/client-warnings/warnings.md b/packages/svelte/messages/client-warnings/warnings.md index 0c75803433bf..6a3c79daa563 100644 --- a/packages/svelte/messages/client-warnings/warnings.md +++ b/packages/svelte/messages/client-warnings/warnings.md @@ -168,72 +168,6 @@ Consider the following code: To fix it, either create callback props to communicate changes, or mark `person` as [`$bindable`]($bindable). -## reset_misuse - -> reset() was invoked and the `` 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 to avoid an infinite loop `error` → `reset` → `error` - -When you call `reset()` Svelte tears down the template inside `` 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. - -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. - -`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. - -If you need to call `reset` inside the `onerror` handler, ensure you fix the broken state first, *then* invoke `reset()`. - -The examples below show do's and don'ts: - -```svelte - - - - - {#if showComponent} - - {/if} - -``` - -```svelte - - { - // Fix the problematic state first - reset(); // This will cause the error to be thrown again and bypass the boundary -}}> - - -``` - -```svelte - - - - - {#snippet failed(error)} - - {/snippet} - -``` - -```svelte - - { - componentState = initialComponentState; // Fix/reset the problematic state first - reset(); // Now the regular template will show without errors -}}> - - -``` - ## select_multiple_invalid_value > The `value` property of a `` element should be an array, but it received a non-array value. The selection will be kept as is. */ @@ -203,6 +181,17 @@ export function state_proxy_equality_mismatch(operator) { } } +/** + * A `` `reset` function only resets the boundary the first time it is called + */ +export function svelte_boundary_reset_noop() { + if (DEV) { + console.warn(`%c[svelte] svelte_boundary_reset_noop\n%cA \`\` \`reset\` function only resets the boundary the first time it is called\nhttps://svelte.dev/e/svelte_boundary_reset_noop`, bold, normal); + } else { + console.warn(`https://svelte.dev/e/svelte_boundary_reset_noop`); + } +} + /** * The `slide` transition does not work correctly for elements with `display: %value%` * @param {string} value diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-onerror/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-onerror/_config.js index 2a18790c611e..092d7ad37d57 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-onerror/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-onerror/_config.js @@ -2,17 +2,12 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; export default test({ - test({ assert, target, warnings }) { + test({ assert, target }) { const btn = target.querySelector('button'); btn?.click(); - assert.throws(() => { - flushSync(); - }, 'error on template render'); - - // Check that the warning is being showed to the user - assert.include(warnings[0], 'reset() was invoked'); + assert.throws(flushSync, 'svelte_boundary_reset_onerror'); // boundary content empty; only button remains assert.htmlEqual(target.innerHTML, ``); From f686128f348e44bc0950ff960d7e497fc2d607bf Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 18 Jun 2025 11:22:35 -0400 Subject: [PATCH 09/11] update changeset --- .changeset/new-dogs-obey.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/new-dogs-obey.md b/.changeset/new-dogs-obey.md index 088d7a946236..aa9a3d73b958 100644 --- a/.changeset/new-dogs-obey.md +++ b/.changeset/new-dogs-obey.md @@ -2,4 +2,4 @@ 'svelte': patch --- -Add a warning when the misuse of `reset` in an `error:boundary` causes an error to be thrown when re-mounting the boundary content +fix: handle error in correct boundary after reset From d78ab67f8527129c91a3c801f55f9b7ceb93ca3f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 16 Jul 2025 20:54:08 -0400 Subject: [PATCH 10/11] regenerate --- documentation/docs/98-reference/.generated/client-warnings.md | 2 +- packages/svelte/messages/client-warnings/warnings.md | 2 +- packages/svelte/src/internal/client/errors.js | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/documentation/docs/98-reference/.generated/client-warnings.md b/documentation/docs/98-reference/.generated/client-warnings.md index 5f7afcbcd935..6f1d677fe951 100644 --- a/documentation/docs/98-reference/.generated/client-warnings.md +++ b/documentation/docs/98-reference/.generated/client-warnings.md @@ -330,7 +330,7 @@ This `reset` function should only be called once. After that, it has no effect (reset = r)}> - {#snippet failed(e)}

oops! {e.message}

diff --git a/packages/svelte/messages/client-warnings/warnings.md b/packages/svelte/messages/client-warnings/warnings.md index a7b51af3981a..123c6833e688 100644 --- a/packages/svelte/messages/client-warnings/warnings.md +++ b/packages/svelte/messages/client-warnings/warnings.md @@ -288,7 +288,7 @@ This `reset` function should only be called once. After that, it has no effect (reset = r)}> - {#snippet failed(e)}

oops! {e.message}

diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index 42b1889cbd3c..937971da5e0b 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -434,6 +434,7 @@ export function svelte_boundary_reset_onerror() { const error = new Error(`svelte_boundary_reset_onerror\nA \`\` \`reset\` function cannot be called while an error is still being handled\nhttps://svelte.dev/e/svelte_boundary_reset_onerror`); error.name = 'Svelte error'; + throw error; } else { throw new Error(`https://svelte.dev/e/svelte_boundary_reset_onerror`); From 8891015e0073cfaf689c9b236a6ab4d164e98832 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 16 Jul 2025 20:58:17 -0400 Subject: [PATCH 11/11] simplify --- packages/svelte/src/internal/client/dom/blocks/boundary.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index c9c0da347d2a..4ea137bfa8b8 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -338,11 +338,7 @@ export class Boundary { onerror?.(error, reset); calling_on_error = false; } catch (error) { - if (this.#effect !== null) { - invoke_error_boundary(error, this.#effect.parent); - } else { - throw error; - } + invoke_error_boundary(error, this.#effect && this.#effect.parent); } finally { set_active_reaction(previous_reaction); } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy