From a2bbfb9173126972b51e501305e33cf822b7f50f Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Wed, 15 Apr 2026 18:12:52 +0200 Subject: [PATCH] fix: abort running obsolete async branches We shouldn't continue executing async work where we know the surrounding branch is destroyed already, it can leave to noisy "derived inter" warnings or even runtime errors ("cannot stringify symbol" when running a template effect with an uninitialized source). Neither should we warn about waterfalls on an already-destroyed async effect. Fixes #18097 (though strictly speaking that particular instance is also fixed by #18117 which fixes the underlying cause for the reruns; this one is necessary in itself though, as shown by the new test) --- .changeset/stupid-baboons-fall.md | 5 +++ .../src/internal/client/dom/blocks/async.js | 38 +++++++++++-------- .../src/internal/client/reactivity/async.js | 13 ++++--- .../internal/client/reactivity/deriveds.js | 2 +- .../src/internal/client/reactivity/effects.js | 17 ++++++--- .../Child.svelte | 6 +++ .../_config.js | 27 +++++++++++++ .../main.svelte | 31 +++++++++++++++ 8 files changed, 111 insertions(+), 28 deletions(-) create mode 100644 .changeset/stupid-baboons-fall.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-obsolete-branch-no-effect-runs/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-obsolete-branch-no-effect-runs/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-obsolete-branch-no-effect-runs/main.svelte diff --git a/.changeset/stupid-baboons-fall.md b/.changeset/stupid-baboons-fall.md new file mode 100644 index 0000000000..66895ad015 --- /dev/null +++ b/.changeset/stupid-baboons-fall.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: abort running obsolete async branches diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index 43af3d8dd3..fd9d0a2964 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -49,23 +49,29 @@ export function async(node, blockers = [], expressions = [], fn) { set_hydrate_node(end); } - flatten(blockers, [], expressions, (values) => { - if (was_hydrating) { - set_hydrating(true); - set_hydrate_node(previous_hydrate_node); - } - - try { - // get values eagerly to avoid creating blocks if they reject - for (const d of values) get(d); - - fn(node, ...values); - } finally { + flatten( + blockers, + [], + expressions, + (values) => { if (was_hydrating) { - set_hydrating(false); + set_hydrating(true); + set_hydrate_node(previous_hydrate_node); } - decrement_pending(); - } - }); + try { + // get values eagerly to avoid creating blocks if they reject + for (const d of values) get(d); + + fn(node, ...values); + } finally { + if (was_hydrating) { + set_hydrating(false); + } + + decrement_pending(); + } + }, + () => decrement_pending() + ); } diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 6aea790c36..134de3a8fc 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -31,8 +31,9 @@ import { aborted } from './effects.js'; * @param {Array<() => any>} sync * @param {Array<() => Promise>} async * @param {(values: Value[]) => any} fn + * @param {() => void} [on_abort] */ -export function flatten(blockers, sync, async, fn) { +export function flatten(blockers, sync, async, fn, on_abort) { const d = is_runes() ? derived : derived_safe_equal; // Filter out already-settled blockers - no need to wait for them @@ -57,12 +58,14 @@ export function flatten(blockers, sync, async, fn) { function finish(values) { restore(); - try { - fn(values); - } catch (error) { - if ((parent.f & DESTROYED) === 0) { + if ((parent.f & DESTROYED) === 0) { + try { + fn(values); + } catch (error) { invoke_error_boundary(error, parent); } + } else { + on_abort?.(); } unset_context(); diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 5af51449ad..01fdd58831 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -239,7 +239,7 @@ export function async_derived(fn, label, location) { recent_async_deriveds.add(signal); setTimeout(() => { - if (recent_async_deriveds.has(signal)) { + if (recent_async_deriveds.has(signal) && (effect.f & DESTROYED) === 0) { w.await_waterfall(/** @type {string} */ (signal.label), location); recent_async_deriveds.delete(signal); } diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 0fad074e6f..e83a49bc60 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -400,13 +400,18 @@ export function deferred_template_effect(fn, sync = [], async = [], blockers = [ var decrement_pending = increment_pending(); } - flatten(blockers, sync, async, (values) => { - create_effect(EFFECT, () => fn(...values.map(get))); - - if (decrement_pending) { - decrement_pending(); + flatten( + blockers, + sync, + async, + (values) => { + create_effect(EFFECT, () => fn(...values.map(get))); + decrement_pending?.(); + }, + () => { + decrement_pending?.(); } - }); + ); } /** diff --git a/packages/svelte/tests/runtime-runes/samples/async-obsolete-branch-no-effect-runs/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-obsolete-branch-no-effect-runs/Child.svelte new file mode 100644 index 0000000000..1d9bdfada2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-obsolete-branch-no-effect-runs/Child.svelte @@ -0,0 +1,6 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-obsolete-branch-no-effect-runs/_config.js b/packages/svelte/tests/runtime-runes/samples/async-obsolete-branch-no-effect-runs/_config.js new file mode 100644 index 0000000000..83364706e5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-obsolete-branch-no-effect-runs/_config.js @@ -0,0 +1,27 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs, warnings }) { + const [increment, resolve] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + assert.deepEqual(logs, []); + + resolve.click(); + await tick(); + assert.deepEqual(logs, []); + + resolve.click(); + await tick(); + assert.deepEqual(logs, []); + + resolve.click(); + await tick(); + assert.deepEqual(logs, [1, 2]); + + // no await waterfall / inert derived warnings + assert.deepEqual(warnings, []); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-obsolete-branch-no-effect-runs/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-obsolete-branch-no-effect-runs/main.svelte new file mode 100644 index 0000000000..fe01ae457e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-obsolete-branch-no-effect-runs/main.svelte @@ -0,0 +1,31 @@ + + + + + + + {#if count % 2 === 0} + {@const double = count * 2} +

true

+ {await push(count)} {double} + + {:else} +

false

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

loading...

+ {/snippet} +