fix: propagate `$effect` errors to `<svelte:boundary>` (#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 `<svelte:boundary>`. 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 <rich.harris@vercel.com>
pull/17681/head
Razin Shafayet 7 months ago committed by GitHub
parent b657029687
commit 45a796b327
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: propagate `$effect` errors to `<svelte:boundary>`

@ -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;

@ -0,0 +1,5 @@
<script>
$effect(() => {
throw new Error('boom');
});
</script>

@ -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, '<p>caught: boom</p>');
}
});

@ -0,0 +1,11 @@
<script>
import Test from './Test.svelte';
</script>
<svelte:boundary>
<Test />
{#snippet failed(e)}
<p>caught: {e.message}</p>
{/snippet}
</svelte:boundary>
Loading…
Cancel
Save