From 71a6515bd648202b2795a80e68e7c9c7ac9ad4ee Mon Sep 17 00:00:00 2001 From: sijie-Z <1683039482@qq.com> Date: Sat, 6 Jun 2026 03:17:49 +0800 Subject: [PATCH] fix: check boundary exists before calling error handler in async derived (#18384) fixes #18383 I traced the issue to `packages/svelte/src/internal/client/error-handling.js`, specifically the `invoke_error_boundary` function. The problem is at line 56 where `effect.b.error(error)` is called without checking if `effect.b` exists. What happens is: when an async `$derived` rejects inside a ``, the error is handled correctly the first time. But if the boundary is destroyed before the rejection fully settles (e.g. component unmounts), a subsequent settle tries to call `effect.b.error()` on a destroyed boundary where `effect.b` is `null`, causing a `TypeError`. The fix adds a null check for `effect.b` before calling `error()`. If the boundary has been destroyed, we skip it and continue bubbling up to the parent boundary. This way the error still gets handled properly instead of crashing. This could also be tested by creating a component with an async `$derived` that rejects after the component unmounts, but I wanted to get the fix up first for review. --------- Co-authored-by: Rich Harris --- .changeset/fix-boundary-null.md | 5 ++++ .../src/internal/client/error-handling.js | 6 ++++- packages/svelte/src/internal/client/index.js | 1 - .../async-error-boundary-4/Child.svelte | 6 +++++ .../samples/async-error-boundary-4/_config.js | 13 +++++++++ .../async-error-boundary-4/main.svelte | 27 +++++++++++++++++++ 6 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-boundary-null.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-error-boundary-4/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-error-boundary-4/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-error-boundary-4/main.svelte diff --git a/.changeset/fix-boundary-null.md b/.changeset/fix-boundary-null.md new file mode 100644 index 0000000000..c87a47ce34 --- /dev/null +++ b/.changeset/fix-boundary-null.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: ignore errors that occur in destroyed effects diff --git a/packages/svelte/src/internal/client/error-handling.js b/packages/svelte/src/internal/client/error-handling.js index 5d16759266..7c404dd933 100644 --- a/packages/svelte/src/internal/client/error-handling.js +++ b/packages/svelte/src/internal/client/error-handling.js @@ -3,7 +3,7 @@ import { DEV } from 'esm-env'; import { FILENAME } from '../../constants.js'; import { is_firefox } from './dom/operations.js'; -import { ERROR_VALUE, BOUNDARY_EFFECT, REACTION_RAN, EFFECT } from './constants.js'; +import { ERROR_VALUE, BOUNDARY_EFFECT, REACTION_RAN, EFFECT, DESTROYED } from './constants.js'; import { define_property, get_descriptor } from '../shared/utils.js'; import { active_effect, active_reaction } from './runtime.js'; @@ -45,6 +45,10 @@ export function handle_error(error) { * @param {Effect | null} effect */ export function invoke_error_boundary(error, effect) { + if (effect !== null && (effect.f & DESTROYED) !== 0) { + return; + } + while (effect !== null) { if ((effect.f & BOUNDARY_EFFECT) !== 0) { if ((effect.f & REACTION_RAN) === 0) { diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 988998d067..fa6f9dda39 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -180,4 +180,3 @@ export { } from '../shared/validate.js'; export { strict_equals, equals } from './dev/equality.js'; export { log_if_contains_state } from './dev/console-log.js'; -export { invoke_error_boundary } from './error-handling.js'; diff --git a/packages/svelte/tests/runtime-runes/samples/async-error-boundary-4/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-error-boundary-4/Child.svelte new file mode 100644 index 0000000000..8bfc51bb7a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-error-boundary-4/Child.svelte @@ -0,0 +1,6 @@ + + +

{model.title}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-error-boundary-4/_config.js b/packages/svelte/tests/runtime-runes/samples/async-error-boundary-4/_config.js new file mode 100644 index 0000000000..a3cda2f865 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-error-boundary-4/_config.js @@ -0,0 +1,13 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [button] = target.querySelectorAll('button'); + + button.click(); + await tick(); + + assert.htmlEqual(target.innerHTML, '

error was contained

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-error-boundary-4/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-error-boundary-4/main.svelte new file mode 100644 index 0000000000..3b5f51742a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-error-boundary-4/main.svelte @@ -0,0 +1,27 @@ + + + + + + {#if open} + + + + {#snippet pending()} +

loading…

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

error was contained

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

error escaped containment

+ {/snippet} +