From b7bc1309aa72cd942fabcf0aa29b9100c6cc7cf1 Mon Sep 17 00:00:00 2001 From: Philip Breuer Date: Tue, 3 Mar 2026 16:20:58 +0100 Subject: [PATCH] fix: preserve original boundary errors when keyed each rows are removed during async updates (#17843) Fixes a runtime edge case where keyed #each reconciliation can hit a missing item during deferred async updates, causing an internal crash and masking the original boundary error. Fixes #17841 ### Before submitting the PR, please make sure you do the following - [x] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs - [x] Prefix your PR title with `feat:`, `fix:`, `chore:`, or `docs:`. - [x] This message body should clearly illustrate what problems it solves. - [x] Ideally, include a test that fails without this PR but passes with it. - [x] If this PR changes code within `packages/svelte/src`, add a changeset (`npx changeset`). ### Tests and linting - [] Run the tests with `pnpm test` and lint the project with `pnpm lint` --------- Co-authored-by: Rich Harris --- .changeset/odd-badgers-camp.md | 5 ++++ .../src/internal/client/dom/blocks/each.js | 6 +++- .../_config.js | 28 ++++++++++++++++++ .../main.svelte | 29 +++++++++++++++++++ 4 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 .changeset/odd-badgers-camp.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-each-const-await-error-boundary/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-each-const-await-error-boundary/main.svelte diff --git a/.changeset/odd-badgers-camp.md b/.changeset/odd-badgers-camp.md new file mode 100644 index 0000000000..ada65f561e --- /dev/null +++ b/.changeset/odd-badgers-camp.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: preserve original boundary errors when keyed each rows are removed during async updates diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index cac980aa9b..b248ce5544 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -35,7 +35,7 @@ import { } from '../../reactivity/effects.js'; import { source, mutable_source, internal_set } from '../../reactivity/sources.js'; import { array_from, is_array } from '../../../shared/utils.js'; -import { BRANCH_EFFECT, COMMENT_NODE, EFFECT_OFFSCREEN, INERT } from '#client/constants'; +import { BRANCH_EFFECT, COMMENT_NODE, DESTROYED, EFFECT_OFFSCREEN, INERT } from '#client/constants'; import { queue_micro_task } from '../task.js'; import { get } from '../../runtime.js'; import { DEV } from 'esm-env'; @@ -217,6 +217,10 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f * @param {Batch} batch */ function commit(batch) { + if ((state.effect.f & DESTROYED) !== 0) { + return; + } + state.pending.delete(batch); state.fallback = fallback; diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-const-await-error-boundary/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each-const-await-error-boundary/_config.js new file mode 100644 index 0000000000..4988e117ed --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-const-await-error-boundary/_config.js @@ -0,0 +1,28 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + // this test doesn't fail without the associated fix — the error gets + // swallowed somewhere. but keeping it around for illustration + skip: true, + + mode: ['client'], + + async test({ assert, target, errors, logs }) { + const button = target.querySelector('button'); + + button?.click(); + await tick(); + await tick(); + assert.deepEqual(logs, ['Simulated TypeError']); + assert.deepEqual(errors, []); + + assert.htmlEqual( + target.innerHTML, + ` + +

Error Caught: Simulated TypeError

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-const-await-error-boundary/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-each-const-await-error-boundary/main.svelte new file mode 100644 index 0000000000..2d8165a9e3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-const-await-error-boundary/main.svelte @@ -0,0 +1,29 @@ + + + + + + {#snippet pending()} +

Loading...

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

Error Caught: {error.message}

+ {/snippet} + + {#each [[1], [2]][index] as id (id)} + {@const result = await fn(id)} +

{result}

+ {/each} +