Skip to content

fix: end a restored reaction context at the end of its synchronous segment - #18694

Open
Nic-Polumeyv wants to merge 4 commits into
mainfrom
async-context-segment
Open

fix: end a restored reaction context at the end of its synchronous segment#18694
Nic-Polumeyv wants to merge 4 commits into
mainfrom
async-context-segment

Conversation

@Nic-Polumeyv

@Nic-Polumeyv Nic-Polumeyv commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

When an async expression resumes after a pickled await, the thunk returned by save() in reactivity/async.js calls restore() to re-arm active_reaction for the rest of the expression, then disarms it with queue_micro_task(unset_context). Any microtask already queued before that one runs inside the restored context. If it writes to a source, set() throws state_unsafe_mutation in production, since the guard is not dev-only. #18453 introduced the queued disarm and noted this case in review as unavoidable. SvelteKit hits it in practice: its fetch continuations write to internal $state (sveltejs/kit#16914), and a user's $derived((await q()).length) resuming in the same tick makes that write throw and drops the update signal.

The context restored by a save thunk now ends with the synchronous segment it was restored in. A restored flag is set by the thunk and consumed on entry to save and track_reactivity_loss, so every suspension ends it; once an expression contains a pickled await, the analysis pickles every later await in it too (has_pickled_await on ExpressionMetadata), so a trailing await compiles to $.save rather than a bare await. At the end of the body, async_thunk in 3-transform/client/utils.js wraps the return expression in $.unsave(...) when the metadata has a pickled await. If the body throws instead, the context is unset by async_derived's existing finally, as before. The queued microtask in save is removed.

Output is unchanged for expressions that pickle nothing ($derived(await a) compiles byte for byte the same). Expressions with a pickled await gain one $.unsave( call per body, and their trailing await becomes a $.save, 4 to 6 bytes gzipped in the added tests. At runtime a boolean write replaces a queued microtask per resume. bench:compare shows no difference outside run-to-run noise.

Two runtime tests reproduce the throw without any library involved, one in dev and one with the prod await shape, and fail on main.

@pkg-svelte-dev

pkg-svelte-dev Bot commented Aug 23, 2026

Copy link
Copy Markdown

Install the latest version of svelte from 23298d5:

pnpm add https://pkg.svelte.dev/svelte/c/23298d5e711d9ddf18dfcec509b7e3709322863b

Open in pkg.svelte.dev: https://pkg.svelte.dev/repos/svelte/pr/18694

@changeset-bot

changeset-bot Bot commented Aug 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 23298d5

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
svelte Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@svelte-docs-bot

Copy link
Copy Markdown

Comment thread packages/svelte/src/compiler/phases/3-transform/client/utils.js Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant