From 89b6a939fe40ac657f27219d29c601fa67202eed Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Tue, 5 May 2026 20:48:50 +0200 Subject: [PATCH 1/2] fix: wrap `Promise.all` in `save` during SSR (#18178) Closes #18168 Not sure if there's a deeper issue in play because the error it's only really there if you also add a title to the component. I think the issue is that with multiple arguments the top level `Promise.all` is not wrapped in `save` and that probably causes a race condition with `title` that sets the context back to `null` in a `finally`. One issue is that now the generated code looks like this ```js const [$$0, $$1] = (await $.save(Promise.all([ (async () => (await $.save(user()))().name)(), (async () => (await $.save(user()))().image)() ])))(); ``` which seems a bit redundant, but I'm not sure if we can get rid of the inner `save` since they are indeed awaiting something. --- .changeset/tough-knives-smell.md | 5 +++++ .../3-transform/server/visitors/shared/utils.js | 4 ++-- .../samples/async-multiple-attrs/_config.js | 8 ++++++++ .../samples/async-multiple-attrs/_expected.html | 1 + .../async-multiple-attrs/_expected_head.html | 1 + .../samples/async-multiple-attrs/main.svelte | 15 +++++++++++++++ 6 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 .changeset/tough-knives-smell.md create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-multiple-attrs/_config.js create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-multiple-attrs/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-multiple-attrs/_expected_head.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-multiple-attrs/main.svelte diff --git a/.changeset/tough-knives-smell.md b/.changeset/tough-knives-smell.md new file mode 100644 index 0000000000..7687188c1a --- /dev/null +++ b/.changeset/tough-knives-smell.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: wrap `Promise.all` in `save` during SSR diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js index a87642bc4c..9b3ac3ad78 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js @@ -12,7 +12,7 @@ import { import * as b from '#compiler/builders'; import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js'; import { regex_whitespaces_strict } from '../../../../patterns.js'; -import { has_await_expression } from '../../../../../utils/ast.js'; +import { has_await_expression, save } from '../../../../../utils/ast.js'; import { ExpressionMetadata } from '../../../../nodes.js'; /** Opens an if/each block, so that we can remove nodes in the case of a mismatch */ @@ -360,7 +360,7 @@ export class PromiseOptimiser { return b.const( b.array_pattern(this.expressions.map((_, i) => b.id(`$$${i}`))), - b.await(b.call('Promise.all', promises)) + save(b.call('Promise.all', promises)) ); } diff --git a/packages/svelte/tests/server-side-rendering/samples/async-multiple-attrs/_config.js b/packages/svelte/tests/server-side-rendering/samples/async-multiple-attrs/_config.js new file mode 100644 index 0000000000..aaf40e7a52 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-multiple-attrs/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + mode: ['async'], + compileOptions: { + dev: true + } +}); diff --git a/packages/svelte/tests/server-side-rendering/samples/async-multiple-attrs/_expected.html b/packages/svelte/tests/server-side-rendering/samples/async-multiple-attrs/_expected.html new file mode 100644 index 0000000000..93016d569c --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-multiple-attrs/_expected.html @@ -0,0 +1 @@ +test \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/async-multiple-attrs/_expected_head.html b/packages/svelte/tests/server-side-rendering/samples/async-multiple-attrs/_expected_head.html new file mode 100644 index 0000000000..96ba2bba28 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-multiple-attrs/_expected_head.html @@ -0,0 +1 @@ +Async multiple attributes \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/async-multiple-attrs/main.svelte b/packages/svelte/tests/server-side-rendering/samples/async-multiple-attrs/main.svelte new file mode 100644 index 0000000000..f14ccd088b --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/async-multiple-attrs/main.svelte @@ -0,0 +1,15 @@ + + + +Async multiple attributes + + +{(await From d4c5a917356a4ef0905681bdd98113c84707db42 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 6 May 2026 09:59:39 +0200 Subject: [PATCH 2/2] fix: rethrow error of failed iterable after calling `return()` (#18169) The fix in #17966 wasn't quite right, because we gotta rethrow in case the iterator stopped because of an error. Fixes part of the SvelteKit `query.live` test failure. --------- Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> Co-authored-by: Rich Harris --- .changeset/fresh-stars-grin.md | 5 +++ .../src/internal/client/reactivity/async.js | 25 ++++++++--- .../_config.js | 24 ++++++++++ .../main.svelte | 45 +++++++++++++++++++ .../_config.js | 21 +++++++++ .../main.svelte | 43 ++++++++++++++++++ 6 files changed, 157 insertions(+), 6 deletions(-) create mode 100644 .changeset/fresh-stars-grin.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws-1/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws-1/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws-2/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws-2/main.svelte diff --git a/.changeset/fresh-stars-grin.md b/.changeset/fresh-stars-grin.md new file mode 100644 index 0000000000..3d56792d1e --- /dev/null +++ b/.changeset/fresh-stars-grin.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: rethrow error of failed iterable after calling `return()` diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 6aea790c36..61fff31f8a 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -213,22 +213,35 @@ export async function* for_await_track_reactivity_loss(iterable) { throw new TypeError('value is not async iterable'); } - /** Whether the completion of the iterator was "normal", meaning it wasn't ended via `break` or a similar method */ - let normal_completion = false; + // eslint-disable-next-line no-useless-assignment + let invoke_return = true; + try { while (true) { const { done, value } = (await track_reactivity_loss(iterator.next()))(); if (done) { - normal_completion = true; + invoke_return = false; break; } var prev = reactivity_loss_tracker; - yield value; + try { + yield value; + } catch (e) { + set_reactivity_loss_tracker(prev); + // If the yield throws, we need to call `return` but not return its value, instead rethrow + if (iterator.return !== undefined) { + (await track_reactivity_loss(iterator.return()))(); + } + throw e; + } set_reactivity_loss_tracker(prev); } + } catch (error) { + invoke_return = false; + throw error; } finally { - // If the iterator had an abrupt completion and `return` is defined on the iterator, call it and return the value - if (!normal_completion && iterator.return !== undefined) { + // If the iterator had an abrupt completion (break) and `return` is defined on the iterator, call it and return the value + if (invoke_return && iterator.return !== undefined) { // eslint-disable-next-line no-unsafe-finally return /** @type {TReturn} */ ((await track_reactivity_loss(iterator.return()))().value); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws-1/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws-1/_config.js new file mode 100644 index 0000000000..9785f639cb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws-1/_config.js @@ -0,0 +1,24 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; +import { normalise_trace_logs } from '../../../helpers.js'; + +export default test({ + compileOptions: { + dev: true + }, + html: '

pending

', + async test({ assert, target, warnings }) { + await tick(); + + assert.htmlEqual( + target.innerHTML, + '

number -> number -> number -> return -> body failed -> ended

' + ); + + assert.deepEqual(normalise_trace_logs(warnings), [ + { + log: 'Detected reactivity loss when reading `values[1]`. This happens when state is read in an async function after an earlier `await`' + } + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws-1/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws-1/main.svelte new file mode 100644 index 0000000000..da7c48642c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws-1/main.svelte @@ -0,0 +1,45 @@ + + + +

{await get_result()}

+ + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws-2/_config.js new file mode 100644 index 0000000000..9e8a2d8def --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws-2/_config.js @@ -0,0 +1,21 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; +import { normalise_trace_logs } from '../../../helpers.js'; + +export default test({ + compileOptions: { + dev: true + }, + html: '

pending

', + async test({ assert, target, warnings }) { + await tick(); + + assert.htmlEqual(target.innerHTML, '

number -> number -> next failed -> ended

'); + + assert.deepEqual(normalise_trace_logs(warnings), [ + { + log: 'Detected reactivity loss when reading `values[1]`. This happens when state is read in an async function after an earlier `await`' + } + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws-2/main.svelte new file mode 100644 index 0000000000..ffe2ef93c0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await-throws-2/main.svelte @@ -0,0 +1,43 @@ + + + +

{await get_result()}

+ + {#snippet pending()} +

pending

+ {/snippet} +