diff --git a/.changeset/polite-peaches-do.md b/.changeset/polite-peaches-do.md new file mode 100644 index 000000000000..0edf72ff32b0 --- /dev/null +++ b/.changeset/polite-peaches-do.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: adds error boundaries diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 8fbce24291bb..058c8329ac48 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -50,17 +50,9 @@ export function boundary(node, boundary_fn, props) { block(() => { var boundary = /** @type {Effect} */ (active_effect); - var start = hydrate_node; // We re-use the effect's fn property to avoid allocation of an additional field - boundary.fn = (/** @type {{ error?: Error }} */ payload) => { - let { error } = payload; - - // In the future, boundaries might handle other things other than errors - if (!error) { - return; - } - + boundary.fn = (/** @type { Error }} */ error) => { var onerror = props.onerror; let failed_snippet = props.failed; diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 7e5c73728bd2..f6984ada3062 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -243,8 +243,10 @@ function propagate_error(error, effect) { if ((current.f & BOUNDARY_EFFECT) !== 0) { try { // @ts-ignore - current.fn({ error }); + current.fn(error); } catch { + // Remove boundary flag from effect + current.f ^= BOUNDARY_EFFECT; current = parent; continue; } @@ -485,7 +487,7 @@ export function update_effect(effect) { dev_effect_stack.push(effect); } } catch (error) { - handle_error(/** @type {Error} */ (error), effect, previous_component_context); + handle_error(/** @type {Error} */ (error), effect, previous_component_context || effect.ctx); } finally { active_effect = previous_effect; @@ -565,22 +567,28 @@ function flush_queued_effects(effects) { for (var i = 0; i < length; i++) { var effect = effects[i]; - if ((effect.f & (DESTROYED | INERT)) === 0 && check_dirtiness(effect)) { - update_effect(effect); - - // Effects with no dependencies or teardown do not get added to the effect tree. - // Deferred effects (e.g. `$effect(...)`) _are_ added to the tree because we - // don't know if we need to keep them until they are executed. Doing the check - // here (rather than in `update_effect`) allows us to skip the work for - // immediate effects. - if (effect.deps === null && effect.first === null && effect.nodes_start === null) { - if (effect.teardown === null) { - // remove this effect from the graph - unlink_effect(effect); - } else { - // keep the effect in the graph, but free up some memory - effect.fn = null; + if ((effect.f & (DESTROYED | INERT)) === 0) { + try { + if (check_dirtiness(effect)) { + update_effect(effect); + + // Effects with no dependencies or teardown do not get added to the effect tree. + // Deferred effects (e.g. `$effect(...)`) _are_ added to the tree because we + // don't know if we need to keep them until they are executed. Doing the check + // here (rather than in `update_effect`) allows us to skip the work for + // immediate effects. + if (effect.deps === null && effect.first === null && effect.nodes_start === null) { + if (effect.teardown === null) { + // remove this effect from the graph + unlink_effect(effect); + } else { + // keep the effect in the graph, but free up some memory + effect.fn = null; + } + } } + } catch (error) { + handle_error(/** @type {Error} */ (error), effect, effect.ctx); } } } @@ -653,8 +661,14 @@ function process_effects(effect, collected_effects) { if ((flags & RENDER_EFFECT) !== 0) { if (is_branch) { current_effect.f ^= CLEAN; - } else if (check_dirtiness(current_effect)) { - update_effect(current_effect); + } else { + try { + if (check_dirtiness(current_effect)) { + update_effect(current_effect); + } + } catch (error) { + handle_error(/** @type {Error} */ (error), current_effect, current_effect.ctx); + } } var child = current_effect.first; diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-12/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-12/_config.js new file mode 100644 index 000000000000..b58e8781ea3c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-12/_config.js @@ -0,0 +1,13 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target, logs }) { + const btn = target.querySelector('button'); + + btn?.click(); + flushSync(); + + assert.htmlEqual(target.innerHTML, `

Error occured

`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-12/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-12/main.svelte new file mode 100644 index 000000000000..bae46c2590dc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-12/main.svelte @@ -0,0 +1,22 @@ + + + + + + {#each things as thing} +

{thing}

+ {/each} + + {#snippet failed()} +

Error occured

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-13/Child.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-13/Child.svelte new file mode 100644 index 000000000000..6e607871d38f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-13/Child.svelte @@ -0,0 +1,7 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-13/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-13/_config.js new file mode 100644 index 000000000000..b58e8781ea3c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-13/_config.js @@ -0,0 +1,13 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target, logs }) { + const btn = target.querySelector('button'); + + btn?.click(); + flushSync(); + + assert.htmlEqual(target.innerHTML, `

Error occured

`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-13/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-13/main.svelte new file mode 100644 index 000000000000..65b71106fab9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-13/main.svelte @@ -0,0 +1,22 @@ + + + + + + + + {#snippet failed()} +

Error occured

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-5/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-5/main.svelte index f32a43b2cc2a..f0d1015ee605 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-boundary-5/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-5/main.svelte @@ -10,7 +10,7 @@ {#snippet failed(e, retry)} -
{e.message}
+
too high
{/snippet}