mirror of https://github.com/sveltejs/svelte
fix: route $derived teardown errors through invoke_error_boundary (#18486)
Fixes #18485 Two parts to this: 1. explicitly invoke error boundary during teardown errors, else they go missing/bubble up outside the render tree 2. skip destroying/destroyed boundaries will searching a handler --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com> Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>pull/18685/head
parent
8bf9ec8700
commit
224fcadbd5
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'svelte': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: route $derived teardown errors through invoke_error_boundary
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
import { assert, test } from 'vitest';
|
||||||
|
import { BOUNDARY_EFFECT, DESTROYED, REACTION_RAN } from './constants';
|
||||||
|
import { invoke_error_boundary } from './error-handling';
|
||||||
|
import type { Effect } from './types';
|
||||||
|
|
||||||
|
test('ignores errors from a destroyed entry effect', () => {
|
||||||
|
const error = new Error('original');
|
||||||
|
let handled = null;
|
||||||
|
const boundary = {
|
||||||
|
f: BOUNDARY_EFFECT | REACTION_RAN,
|
||||||
|
b: { error: (error: unknown) => (handled = error) },
|
||||||
|
parent: null
|
||||||
|
} as unknown as Effect;
|
||||||
|
const effect = { f: DESTROYED, parent: boundary } as Effect;
|
||||||
|
|
||||||
|
invoke_error_boundary(error, effect);
|
||||||
|
|
||||||
|
assert.equal(handled, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('skips destroyed boundary ancestors without masking the error', () => {
|
||||||
|
const error = new Error('original');
|
||||||
|
let handled = null;
|
||||||
|
const live_boundary = {
|
||||||
|
f: BOUNDARY_EFFECT | REACTION_RAN,
|
||||||
|
b: { error: (error: unknown) => (handled = error) },
|
||||||
|
parent: null
|
||||||
|
} as unknown as Effect;
|
||||||
|
const destroyed_boundary = {
|
||||||
|
f: BOUNDARY_EFFECT | DESTROYED | REACTION_RAN,
|
||||||
|
b: null,
|
||||||
|
parent: live_boundary
|
||||||
|
} as unknown as Effect;
|
||||||
|
const effect = { f: 0, parent: destroyed_boundary } as Effect;
|
||||||
|
|
||||||
|
invoke_error_boundary(error, effect);
|
||||||
|
|
||||||
|
assert.equal(handled, error);
|
||||||
|
});
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<script>
|
||||||
|
let { getValue } = $props();
|
||||||
|
|
||||||
|
// Reads reactive state during teardown
|
||||||
|
$effect(() => {
|
||||||
|
return () => {
|
||||||
|
getValue();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span>trigger</span>
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
import { flushSync, tick } from 'svelte';
|
||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
// Regression test for https://github.com/sveltejs/svelte/issues/18485.
|
||||||
|
// A $derived that re-executes and throws during teardown should route the
|
||||||
|
// error to a live ancestor boundary.
|
||||||
|
mode: ['client'],
|
||||||
|
async test({ assert, target }) {
|
||||||
|
const [break_it, unmount] = target.querySelectorAll('button');
|
||||||
|
|
||||||
|
break_it.click();
|
||||||
|
flushSync();
|
||||||
|
|
||||||
|
assert.doesNotThrow(() => {
|
||||||
|
unmount.click();
|
||||||
|
flushSync();
|
||||||
|
});
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
assert.htmlEqual(target.innerHTML, '<p>caught</p>');
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
<script>
|
||||||
|
import Trigger from './Trigger.svelte';
|
||||||
|
|
||||||
|
let mounted = $state(true);
|
||||||
|
let broken = $state(false);
|
||||||
|
|
||||||
|
// Throwing derived — nothing in the live template reads `appContext`, so
|
||||||
|
// invalidating it does NOT throw immediately (becomes dirty-and-unread).
|
||||||
|
let data = $derived(broken ? null : { value: 'ok' });
|
||||||
|
let appContext = $derived(data.value);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:boundary>
|
||||||
|
<button onclick={() => (broken = true)}>break</button>
|
||||||
|
<button onclick={() => (mounted = false)}>unmount</button>
|
||||||
|
|
||||||
|
{#if mounted}
|
||||||
|
<Trigger getValue={() => appContext} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#snippet failed()}
|
||||||
|
<p>caught</p>
|
||||||
|
{/snippet}
|
||||||
|
</svelte:boundary>
|
||||||
Loading…
Reference in new issue