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