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