fix: properly hydrate already-resolved async blocks
#17611
Closing issues
Describe the bug
Since v5.43.0, some items in an {#each} block are duplicating in a specific scenario related to the experimental async syntax. All awaited items except the first appear a second time (eg. item 1, item 2, item 3, item 2, item 3). The items are not duplicated in the SSR ouput (in a SvelteKit app), the duplicated items are only added on client load, it appears.
Note: This only occurs in a production build, not in development. No errors/warnings are logged. The issue does not occur in v5.42.3 or below.
Reproduction
The following is a minimal reproduction of an issue I am encountering in a real app. The issue does not occur if const another = {...} (L5) is moved before const messages = await ... (L4), if another is not passed to the component, or if the <p>{message}</p> is defined directly in the {#each} block, instead of in a seperate component.
(you need to use it with hydration locally in the playground to reproduce; for an in-browser reproduction see https://stackblitz.com/edit/sveltejs-kit-template-default-es5urw13?file=src%2Froutes%2F%2Bpage.svelte)
Logs
System Info
System:
OS: Linux 5.0 undefined
CPU: (8) x64 Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
Memory: 0 Bytes / 0 Bytes
Shell: 1.0 - /bin/jsh
Binaries:
Node: 20.19.1 - /usr/local/bin/node
Yarn: 1.22.19 - /usr/local/bin/yarn
npm: 10.8.2 - /usr/local/bin/npm
pnpm: 8.15.6 - /usr/local/bin/pnpm
npmPackages:
svelte: 5.43.0 => 5.43.0
Severity
blocking an upgrade
Describe the bug
TL;DR: Using experimental async ($derived(await ...)) in a component that also uses {@attach} with a function from bind:this breaks SSR hydration. In development, this causes a warning. In production build with nested components, the application crashes completely and the page fails to render.
The issue appears to be that the async boundary interacts incorrectly with the {@attach} directive during hydration. When the component contains $derived(await ...), the hydration process seems to lose track of the attachment state, causing a mismatch between server-rendered HTML and client-side component tree. With nested {@render children?.()} calls, this mismatch escalates into a fatal TypeError that prevents the component from mounting.
Conditions to reproduce
All of the following must be present:
-
Experimental async enabled in
svelte.config.js:compilerOptions: { experimental: { async: true } } -
$derived(await ...)in the component (even if the value is not used in markup) -
{@attach ref?.action}where:
refis obtained viabind:thisfrom another component- The function is exported from that component
-
SSR enabled (default in SvelteKit)
-
Nested components with
{@render children?.()}— for TypeError to occur, at least two levels of nesting required
Behavior
| Environment | Single nesting | Double nesting |
|---|---|---|
| Dev | hydration_mismatch warning | hydration_mismatch warning |
| Production | hydration_mismatch warning | Application crashes, component fails to render |
Notes
- Removing $derived(await ...) — no error
- Removing {@attach} — no error
- Removing
bind:thisreference (using local function instead) — no error - Disabling SSR — no error
Expected
No hydration mismatch when the async value is not used in the SSR-rendered markup, and no TypeError regardless of component nesting depth.
Reproduction
svelte.config.js
export default {
compilerOptions: {
experimental: { async: true }
}
}
index.svelte
<script>
import Outer from './Outer.svelte'
import Inner from './Inner.svelte'
import Trigger from './Trigger.svelte'
const data = $derived(await Promise.resolve(['a', 'b']))
let trigger = $state()
</script>
<Outer>
<Inner {@attach trigger?.action}>
foo
</Inner>
</Outer>
<Trigger bind:this={trigger} />
Outer.svelte
<script>
let { children } = $props()
</script>
<div>{@render children?.()}</div>
Inner.svelte
<script>
let { children } = $props()
</script>
<div>{@render children?.()}</div>
Trigger.svelte
<script lang='ts'>
import type { Attachment } from 'svelte/attachments'
export function action(): Attachment<HTMLElement> {
return ()=>{}
}
</script>
Logs
https://svelte.dev/e/hydration_mismatch
TypeError: Cannot read properties of undefined (reading 'f')
System Info
System:
OS: macOS 26.2
CPU: (10) arm64 Apple M1 Pro
Memory: 147.34 MB / 16.00 GB
Shell: 5.9 - /bin/zsh
Binaries:
Node: 24.13.0 - /Users/exentrich/.nvm/versions/node/v24.13.0/bin/node
Yarn: 1.22.19 - /usr/local/bin/yarn
npm: 11.6.2 - /Users/exentrich/.nvm/versions/node/v24.13.0/bin/npm
bun: 1.3.7 - /Users/exentrich/.bun/bin/bun
Deno: 2.0.4 - /Users/exentrich/.deno/bin/deno
Browsers:
Brave Browser: 141.1.83.120
Chrome: 144.0.7559.110
Chrome Canary: 146.0.7666.1
Edge: 142.0.3595.80
Firefox: 146.0.1
Safari: 26.2
Safari Technology Preview: 26.0
npmPackages:
svelte: ^5.48.2 => 5.49.1
Severity
blocking an upgrade
Describe the bug
Using a nested {#if}{/if} on an async component while having the experimental async feature enabled, breaks hydration when using SSR.
It seems like it fails to find the hydration markers, using whatever else it finds instead.
Reproduction
https://github.com/santiagocezar/hydration-troubles-repro
It seems to be a regression in 5.49.2. Plus, I checked if PR #17611 fixed it by chance but had no luck there, sadly.
There's three variants!
- The sync one is the boring one that just works
- The async-only one causes a hydration mismatch (but the error log doesn't give much details as to why)
- The async+onclick one causes a completely different error! (it grabs a text node for some reason)
Logs
# on the Async-only variation
[svelte] hydration_mismatch
Hydration failed because the initial UI does not match what was rendered on the server
https://svelte.dev/e/hydration_mismatch
Uncaught (in promise) Object { }
# on the Async+onclick one
Uncaught (in promise) DOMException: Node.appendChild: Cannot add children to a Text
System Info
System:
OS: Linux 6.18 cpe:/o:nixos:nixos:26.05 26.05 (Yarara)
CPU: (16) x64 AMD Ryzen 7 7730U with Radeon Graphics
Memory: 4.65 GB / 14.98 GB
Container: Yes
Shell: 0.109.1 - /run/current-system/sw/bin/nu
Binaries:
Node: 24.13.0 - /run/current-system/sw/bin/node
npm: 11.6.2 - /run/current-system/sw/bin/npm
pnpm: 10.28.0 - /run/current-system/sw/bin/pnpm
bun: 1.3.6 - /run/current-system/sw/bin/bun
Browsers:
Firefox: 147.0.1
Firefox Developer Edition: 147.0.1
npmPackages:
svelte: 5.49.2 => 5.49.2
Severity
blocking an upgrade
Pull request
The problem comes down to $.async wrapping a component and being the sole child of a block (if, snippet prop, etc). Normally, said component would either have no content at all, and therefore nothing to advance with respect to hydration nodes, or call $.append() at the end which advances it past the closing hydration anchor. But when $.async wraps it, there's now an additional comment pair surrounding them, and it's not advancing past those - so the next hydration walk assumes its now past the closing marker when it isn't. For this we need to add $.next() after $.async()
Also fixes #17618
The problem here is very related (hence amended to the PR) where with fast-path, it is unknown to us within $.async whether or not the marker will advance to the closing marker of $.async - with a component as its child it will (because of the $.append inside it), with an if block for example it will not (because the if block logic will stop at its closing marker). We cannot know which case it is hence we advance to the end marker before calling the inner function in $.async, to then definitly set $.async's closing marker.
Before submitting the PR, please make sure you do the following
- It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
- Prefix your PR title with
feat:,fix:,chore:, ordocs:. - This message body should clearly illustrate what problems it solves.
- Ideally, include a test that fails without this PR but passes with it.
- If this PR changes code within
packages/svelte/src, add a changeset (npx changeset).
Tests and linting
- Run the tests with
pnpm testand lint the project withpnpm lint
Info
🦋 Changeset detected
Latest commit: 91a6eb0
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
pnpm add https://pkg.pr.new/svelte@17611We were missing a "skip nodes to the end" invocation Fixes #17261
The problem comes down to $.async wrapping a component and being the sole child of a block (if, snippet prop, etc). Normally, said component would either have no content at all, and therefore nothing to advance with respect to hydration nodes, or call $.append() at the end which advances it past the closing hydration anchor. But when $.async wraps it, there's now an additional comment pair surrounding them, and it's not advancing in this case. For this we need to add $.next() after $.async()
ensure async blocks end on correct hydration marker, fixes #17618
• Feb 5, 2026, 10:06 PMPro tip: You can prefix GitHub URLs of issues, PRs or discussions with svcl.dev/ to view them on this page! Also try it on a GitHub release ;)