From 1dcced59e79dd04163ce4aa380948ca6eea777af Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 18 Aug 2025 19:10:08 +0200 Subject: [PATCH] fix: properly catch top level await errors (#16619) * fix: properly catch top level await errors async errors within the template and derived etc are properly handled because they know about the last active effect and invoke the error boundary correctly as a response. This logic was missing for our top level await output. Fixes #16613 * test * use helper for async bodies (#16641) * use helper for async bodies * unused * fix * failing test + fix --------- Co-authored-by: Simon Holthausen --------- Co-authored-by: Rich Harris --- .changeset/silent-suns-whisper.md | 5 ++ .../3-transform/client/transform-client.js | 60 +++++++------------ packages/svelte/src/internal/client/index.js | 5 +- .../src/internal/client/reactivity/async.js | 21 ++++++- .../src/internal/client/reactivity/effects.js | 3 +- .../Child.svelte | 9 +++ .../_config.js | 15 +++++ .../main.svelte | 18 ++++++ .../async-top-level-error-nested/Child.svelte | 7 +++ .../async-top-level-error-nested/_config.js | 14 +++++ .../async-top-level-error-nested/main.svelte | 18 ++++++ 11 files changed, 131 insertions(+), 44 deletions(-) create mode 100644 .changeset/silent-suns-whisper.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/main.svelte diff --git a/.changeset/silent-suns-whisper.md b/.changeset/silent-suns-whisper.md new file mode 100644 index 0000000000..7ee7d74abc --- /dev/null +++ b/.changeset/silent-suns-whisper.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: properly catch top level await errors diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 166207f66a..940d6a9e00 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -359,16 +359,31 @@ export function client_component(analysis, options) { if (dev) push_args.push(b.id(analysis.name)); let component_block = b.block([ + store_init, ...store_setup, ...legacy_reactive_declarations, ...group_binding_declarations, - ...state.instance_level_snippets, - .../** @type {ESTree.Statement[]} */ (instance.body), - analysis.runes || !analysis.needs_context - ? b.empty - : b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined)) + ...state.instance_level_snippets ]); + if (analysis.instance.has_await) { + const body = b.block([ + .../** @type {ESTree.Statement[]} */ (instance.body), + b.if(b.call('$.aborted'), b.return()), + .../** @type {ESTree.Statement[]} */ (template.body) + ]); + + component_block.body.push(b.stmt(b.call(`$.async_body`, b.arrow([], body, true)))); + } else { + component_block.body.push(.../** @type {ESTree.Statement[]} */ (instance.body)); + + if (!analysis.runes && analysis.needs_context) { + component_block.body.push(b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined))); + } + + component_block.body.push(.../** @type {ESTree.Statement[]} */ (template.body)); + } + if (analysis.needs_mutation_validation) { component_block.body.unshift( b.var('$$ownership_validator', b.call('$.create_ownership_validator', b.id('$$props'))) @@ -389,41 +404,6 @@ export function client_component(analysis, options) { analysis.uses_slots || analysis.slot_names.size > 0; - if (analysis.instance.has_await) { - const params = [b.id('$$anchor')]; - if (should_inject_props) { - params.push(b.id('$$props')); - } - if (store_setup.length > 0) { - params.push(b.id('$$stores')); - } - const body = b.function_declaration( - b.id('$$body'), - params, - b.block([ - b.var('$$unsuspend', b.call('$.suspend')), - ...component_block.body, - b.if(b.call('$.aborted'), b.return()), - .../** @type {ESTree.Statement[]} */ (template.body), - b.stmt(b.call('$$unsuspend')) - ]), - true - ); - - state.hoisted.push(body); - - component_block = b.block([ - b.var('fragment', b.call('$.comment')), - b.var('node', b.call('$.first_child', b.id('fragment'))), - store_init, - b.stmt(b.call(body.id, b.id('node'), ...params.slice(1))), - b.stmt(b.call('$.append', b.id('$$anchor'), b.id('fragment'))) - ]); - } else { - component_block.body.unshift(store_init); - component_block.body.push(.../** @type {ESTree.Statement[]} */ (template.body)); - } - // trick esrap into including comments component_block.loc = instance.loc; diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index c094c9e044..c5b7bb845c 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -99,6 +99,7 @@ export { with_script } from './dom/template.js'; export { + async_body, for_await_track_reactivity_loss, save, track_reactivity_loss @@ -151,7 +152,8 @@ export { untrack, exclude_from_object, deep_read, - deep_read_state + deep_read_state, + active_effect } from './runtime.js'; export { validate_binding, validate_each_keys } from './validate.js'; export { raf } from './timing.js'; @@ -176,3 +178,4 @@ 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/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 2b133e5f44..1ea1bbe561 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -11,7 +11,7 @@ import { set_active_effect, set_active_reaction } from '../runtime.js'; -import { current_batch } from './batch.js'; +import { current_batch, suspend } from './batch.js'; import { async_derived, current_async_effect, @@ -19,6 +19,7 @@ import { derived_safe_equal, set_from_async_derived } from './deriveds.js'; +import { aborted } from './effects.js'; /** * @@ -170,3 +171,21 @@ export function unset_context() { set_component_context(null); if (DEV) set_from_async_derived(null); } + +/** + * @param {() => Promise} fn + */ +export async function async_body(fn) { + const unsuspend = suspend(); + const active = /** @type {Effect} */ (active_effect); + + try { + await fn(); + } catch (error) { + if (!aborted(active)) { + invoke_error_boundary(error, active); + } + } finally { + unsuspend(); + } +} diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 68a1555032..df3dd75808 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -648,7 +648,6 @@ function resume_children(effect, local) { } } -export function aborted() { - var effect = /** @type {Effect} */ (active_effect); +export function aborted(effect = /** @type {Effect} */ (active_effect)) { return (effect.f & DESTROYED) !== 0; } diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/Child.svelte new file mode 100644 index 0000000000..f7ba132ace --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/Child.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/_config.js b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/_config.js new file mode 100644 index 0000000000..298e33e9a2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/_config.js @@ -0,0 +1,15 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

pending

`, + + async test({ assert, target }) { + const [reject] = target.querySelectorAll('button'); + + await tick(); + reject.click(); + await tick(); + assert.htmlEqual(target.innerHTML, '

route: other

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/main.svelte new file mode 100644 index 0000000000..2f461e96c8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/main.svelte @@ -0,0 +1,18 @@ + + + + + + {#if route.current === 'home'} + + {:else} +

route: {route.current}

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

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/Child.svelte new file mode 100644 index 0000000000..11c9ebd653 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/Child.svelte @@ -0,0 +1,7 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/_config.js b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/_config.js new file mode 100644 index 0000000000..57005b4112 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/_config.js @@ -0,0 +1,14 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

pending

`, + + async test({ assert, target }) { + const [reject] = target.querySelectorAll('button'); + + reject.click(); + await tick(); + assert.htmlEqual(target.innerHTML, '

failed

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/main.svelte new file mode 100644 index 0000000000..2fdf4c0d2f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/main.svelte @@ -0,0 +1,18 @@ + + + + + + + + {#snippet pending()} +

pending

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

failed

+ {/snippet} +