fix: run boundary `onerror` callbacks in a microtask, in case they result in the boundary's destruction (#17561)

* fix: mark parent effect as dirty on error boundary initialization

This change ensures that if an error occurs while a parent effect is still initializing, the parent effect is marked as dirty and scheduled to re-run. This addresses scenarios where state updates in the onerror handler do not trigger a re-evaluation of the condition, improving error handling in Svelte components.

* chore: add changeset for #17553

* fix: run boundary  callbacks in a microtask, in case they result in the boundary's destruction

---------

Co-authored-by: frozenflux2 <snowflake30518@gmail.com>
pull/17560/head
Rich Harris 6 months ago committed by GitHub
parent 8c8a22add4
commit 30cea1f9a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: run boundary `onerror` callbacks in a microtask, in case they result in the boundary's destruction

@ -1,5 +1,6 @@
/** @import { Effect, Source, TemplateNode, } from '#client' */
import {
BLOCK_EFFECT,
BOUNDARY_EFFECT,
COMMENT_NODE,
DIRTY,
@ -449,21 +450,16 @@ export class Boundary {
}
};
var previous_reaction = active_reaction;
try {
set_active_reaction(null);
calling_on_error = true;
onerror?.(error, reset);
calling_on_error = false;
} catch (error) {
invoke_error_boundary(error, this.#effect && this.#effect.parent);
} finally {
set_active_reaction(previous_reaction);
}
queue_micro_task(() => {
try {
calling_on_error = true;
onerror?.(error, reset);
calling_on_error = false;
} catch (error) {
invoke_error_boundary(error, this.#effect && this.#effect.parent);
}
if (failed) {
queue_micro_task(() => {
if (failed) {
this.#failed_effect = this.#run(() => {
Batch.ensure();
this.#is_creating_fallback = true;
@ -483,8 +479,8 @@ export class Boundary {
this.#is_creating_fallback = false;
}
});
});
}
}
});
}
}

@ -0,0 +1,5 @@
<script>
throw new Error('child error');
</script>
<p>Child content</p>

@ -0,0 +1,13 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client'],
test({ assert, target }) {
flushSync();
// When exception is set by onerror, the {#if !exception} block should hide
// and only the {#if exception} block should be visible
assert.htmlEqual(target.innerHTML, '<p>caught error: child error</p>');
}
});

@ -0,0 +1,19 @@
<script>
import Child from './Child.svelte';
let exception = $state();
const onerror = (e) => {
exception = e;
};
</script>
{#if !exception}
<p>condition is {String(!exception)}</p>
<svelte:boundary {onerror}>
<Child />
</svelte:boundary>
{/if}
{#if exception}
<p>caught error: {exception.message}</p>
{/if}

@ -0,0 +1,8 @@
<script>
// it's important for the test that this isn't an `Error`
throw 'child error'
</script>
<p>
boom
</p>

@ -0,0 +1,13 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client'],
test({ assert, target }) {
flushSync();
// When exception is set by onerror, the {#if !exception} block should hide
// and only the {#if exception} block should be visible
assert.htmlEqual(target.innerHTML, '<p>caught error: child error</p>');
}
});

@ -0,0 +1,19 @@
<script>
import Child from './Child.svelte';
let exception = $state();
const onerror = (e) => {
exception = e;
};
</script>
{#if !exception}
<p>condition is {String(!exception)}</p>
<svelte:boundary {onerror}>
<Child />
</svelte:boundary>
{/if}
{#if exception}
<p>caught error: {exception}</p>
{/if}
Loading…
Cancel
Save