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} +