From 45a796b32722878a03303eb389a637d5c6e047f7 Mon Sep 17 00:00:00 2001 From: Razin Shafayet Date: Fri, 13 Feb 2026 04:23:43 +0600 Subject: [PATCH] fix: propagate `$effect` errors to `` (#17684) Fixes #16342. Errors thrown inside `$effect` were previously treated as subtree-creation errors when `EFFECT_RAN === 0`, which caused them to be rethrown instead of propagating to the nearest ``. As a result, `$effect` errors bypassed boundaries and appeared as uncaught runtime errors. This change ensures that errors originating from effects (`EFFECT`) are routed through `invoke_error_boundary`, allowing them to bubble up the effect tree and be handled correctly by the closest boundary. Existing subtree-creation behavior for non-effect cases remains unchanged. --- ### Before submitting the PR, please make sure you do the following - [x] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs - [x] Prefix your PR title with `feat:`, `fix:`, `chore:`, or `docs:`. - [x] This message body should clearly illustrate what problems it solves. - [x] Ideally, include a test that fails without this PR but passes with it. - [x] If this PR changes code within `packages/svelte/src`, add a changeset (`npx changeset`). ### Tests and linting - [x] Run the tests with `pnpm test` and lint the project with `pnpm lint` --------- Co-authored-by: Rich Harris --- .changeset/short-worlds-enter.md | 5 ++++ .../src/internal/client/error-handling.js | 30 ++++++++++--------- .../samples/boundary-effect-error/Test.svelte | 5 ++++ .../samples/boundary-effect-error/_config.js | 10 +++++++ .../samples/boundary-effect-error/main.svelte | 11 +++++++ 5 files changed, 47 insertions(+), 14 deletions(-) create mode 100644 .changeset/short-worlds-enter.md create mode 100644 packages/svelte/tests/runtime-runes/samples/boundary-effect-error/Test.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/boundary-effect-error/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/boundary-effect-error/main.svelte diff --git a/.changeset/short-worlds-enter.md b/.changeset/short-worlds-enter.md new file mode 100644 index 0000000000..96e0f80370 --- /dev/null +++ b/.changeset/short-worlds-enter.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: propagate `$effect` errors to `` diff --git a/packages/svelte/src/internal/client/error-handling.js b/packages/svelte/src/internal/client/error-handling.js index 4736333d1a..5d16759266 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 } from './constants.js'; +import { ERROR_VALUE, BOUNDARY_EFFECT, REACTION_RAN, EFFECT } from './constants.js'; import { define_property, get_descriptor } from '../shared/utils.js'; import { active_effect, active_reaction } from './runtime.js'; @@ -25,22 +25,19 @@ export function handle_error(error) { adjustments.set(error, get_adjustments(error, effect)); } - if ((effect.f & REACTION_RAN) === 0) { - // if the error occurred while creating this subtree, we let it - // bubble up until it hits a boundary that can handle it - if ((effect.f & BOUNDARY_EFFECT) === 0) { - if (DEV && !effect.parent && error instanceof Error) { - apply_adjustments(error); - } - - throw error; + // if the error occurred while creating this subtree, we let it + // bubble up until it hits a boundary that can handle it, unless + // it's an $effect in which case it doesn't run immediately + if ((effect.f & REACTION_RAN) === 0 && (effect.f & EFFECT) === 0) { + if (DEV && !effect.parent && error instanceof Error) { + apply_adjustments(error); } - /** @type {Boundary} */ (effect.b).error(error); - } else { - // otherwise we bubble up the effect tree ourselves - invoke_error_boundary(error, effect); + throw error; } + + // otherwise we bubble up the effect tree ourselves + invoke_error_boundary(error, effect); } /** @@ -50,6 +47,11 @@ export function handle_error(error) { export function invoke_error_boundary(error, effect) { while (effect !== null) { if ((effect.f & BOUNDARY_EFFECT) !== 0) { + if ((effect.f & REACTION_RAN) === 0) { + // we are still creating the boundary effect + throw error; + } + try { /** @type {Boundary} */ (effect.b).error(error); return; diff --git a/packages/svelte/tests/runtime-runes/samples/boundary-effect-error/Test.svelte b/packages/svelte/tests/runtime-runes/samples/boundary-effect-error/Test.svelte new file mode 100644 index 0000000000..4aec88b712 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/boundary-effect-error/Test.svelte @@ -0,0 +1,5 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/boundary-effect-error/_config.js b/packages/svelte/tests/runtime-runes/samples/boundary-effect-error/_config.js new file mode 100644 index 0000000000..4d2728abc1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/boundary-effect-error/_config.js @@ -0,0 +1,10 @@ +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + // allow effects to run / microtasks to flush + await Promise.resolve(); + + assert.htmlEqual(target.innerHTML, '

caught: boom

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/boundary-effect-error/main.svelte b/packages/svelte/tests/runtime-runes/samples/boundary-effect-error/main.svelte new file mode 100644 index 0000000000..63968f6b35 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/boundary-effect-error/main.svelte @@ -0,0 +1,11 @@ + + + + + + {#snippet failed(e)} +

caught: {e.message}

+ {/snippet} +