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
pull/18271/head
Simon H 5 days ago committed by GitHub
parent a6002b587c
commit 078f901f61
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: catch rejected promises while merging/committing

@ -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);
}
}

@ -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, '<button>increment</button> done');
}
});

@ -0,0 +1,21 @@
<script>
let count = $state(0);
const queued = [];
function push(v) {
if (v === 0) return v;
return new Promise((fulfil) => {
queued.push(() => fulfil(v));
});
}
</script>
<button onclick={() => count++}>increment</button>
{#if count < 3}
{await push(count)}
{:else}
done
{/if}

@ -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, '<button>increment</button><button>pop</button> failed');
pop.click();
await tick();
pop.click();
await tick();
assert.htmlEqual(target.innerHTML, '<button>increment</button><button>pop</button> failed');
}
});

@ -0,0 +1,22 @@
<script>
let count = $state(0);
const queued = [];
function push(v) {
if (v === 0) return v;
return new Promise((fulfil,reject) => {
queued.push(() => v === 3 ? reject('boom') : fulfil(v));
});
}
</script>
<button onclick={() => count++}>increment</button>
<button onclick={() => queued.pop()?.()}>pop</button>
<svelte:boundary>
{await push(count)}
{#snippet failed()}failed{/snippet}
</svelte:boundary>
Loading…
Cancel
Save