From 078f901f611237d4c6fed16ef894f33572f9ccba Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 21 May 2026 20:27:13 +0200 Subject: [PATCH] fix: catch rejected promises while merging/committing (#18266) A committing/merging batch can have promises that were rejected (e.g. as obsolete). We gotta "forward" this rejection, too, instead of just the successful promise. At best it results in a uncaught rejection (`async-branch-merge-obsolete`), at worst it means error boundaries are not correctly displayed (`async-later-promise-fails-first`). Solves the reproduction in https://github.com/sveltejs/svelte/issues/18221#issuecomment-4507803845 --- .changeset/true-pigs-go.md | 5 ++++ .../src/internal/client/reactivity/batch.js | 4 +-- .../async-branch-merge-obsolete/_config.js | 17 +++++++++++++ .../async-branch-merge-obsolete/main.svelte | 21 ++++++++++++++++ .../_config.js | 25 +++++++++++++++++++ .../main.svelte | 22 ++++++++++++++++ 6 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 .changeset/true-pigs-go.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-branch-merge-obsolete/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-branch-merge-obsolete/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-later-promise-fails-first/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-later-promise-fails-first/main.svelte diff --git a/.changeset/true-pigs-go.md b/.changeset/true-pigs-go.md new file mode 100644 index 0000000000..b4900b38fa --- /dev/null +++ b/.changeset/true-pigs-go.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: catch rejected promises while merging/committing diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index b9721b6243..82c97cf95c 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -510,7 +510,7 @@ export class Batch { for (const [effect, deferred] of batch.async_deriveds) { const d = this.async_deriveds.get(effect); - if (d) deferred.promise.then(d.resolve); + if (d) deferred.promise.then(d.resolve).catch(d.reject); } // Mark is not guaranteed not touch these, so we transfer them @@ -677,7 +677,7 @@ export class Batch { // immediately resolving them? Likely not because of how this.apply() works. for (const [effect, deferred] of this.async_deriveds) { const d = batch.async_deriveds.get(effect); - if (d) deferred.promise.then(d.resolve); + if (d) deferred.promise.then(d.resolve).catch(d.reject); } } diff --git a/packages/svelte/tests/runtime-runes/samples/async-branch-merge-obsolete/_config.js b/packages/svelte/tests/runtime-runes/samples/async-branch-merge-obsolete/_config.js new file mode 100644 index 0000000000..faf1ff7f6b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-branch-merge-obsolete/_config.js @@ -0,0 +1,17 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [increment] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + increment.click(); + await tick(); + increment.click(); + await tick(); + assert.htmlEqual(target.innerHTML, ' done'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-branch-merge-obsolete/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-branch-merge-obsolete/main.svelte new file mode 100644 index 0000000000..4780442293 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-branch-merge-obsolete/main.svelte @@ -0,0 +1,21 @@ + + + + +{#if count < 3} + {await push(count)} +{:else} + done +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-later-promise-fails-first/_config.js b/packages/svelte/tests/runtime-runes/samples/async-later-promise-fails-first/_config.js new file mode 100644 index 0000000000..5e18c953c7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-later-promise-fails-first/_config.js @@ -0,0 +1,25 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [increment, pop] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + increment.click(); + await tick(); + increment.click(); + await tick(); + pop.click(); + await tick(); + assert.htmlEqual(target.innerHTML, ' failed'); + + pop.click(); + await tick(); + pop.click(); + await tick(); + assert.htmlEqual(target.innerHTML, ' failed'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-later-promise-fails-first/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-later-promise-fails-first/main.svelte new file mode 100644 index 0000000000..3293e4ab88 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-later-promise-fails-first/main.svelte @@ -0,0 +1,22 @@ + + + + + + + {await push(count)} + + {#snippet failed()}failed{/snippet} +