Repository: sveltejs/svelte (2026-03-31T21:06:12Z)

Issue:
Async Svelte crash in certain situations when destructuring awaited derived value in a @const
### Describe the bug

This seems to happen when you have:

First, an awaited query, and a derived expression from that query
```typescript
import { getIt } from './query.remote.ts';
let d = $derived(await getIt());
// For some reason this additional derived is required to reproduce
let showFetchCta = $derived(d.hasData)
```

In the template, a `@const` statement that destructures the awaited value.

```svelte
{#if d}
	{@const {data, hasData} = d}

	{#if hasData}
		<p>The Data: {data}</p>
	{:else if showFetchCta} <!-- reference the derived so it doesn't get thrown away -->
		<p>Fetch now</p>
	{:else}
		<p>No data</p>
	{/if}
{/if}
```

Causes the error
```
 TypeError: Cannot read properties of undefined (reading 'f')
```

In Module.get where it reads `signal.f` (`signal` is apparently still undefined).

If you remove the `@const` destructuring and just reference the values as `d.data` and so on, then it works fine.


### Reproduction

https://www.sveltelab.dev/38gut2mu4bw9p4d

Note you might have to run with the inspector open to really see the crash. Otherwise it's not totally obvious all the time.

### Logs

```shell
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'f')

	in <unknown>
	in +page.svelte
	in +layout.svelte
	in root.svelte
	in undefined

    at Module.get (chunk-O3RU4Q5E.js?v=4ba18bce:4157:23)
    at +page.svelte?t=1774689309840:99:13
    at chunk-GMPPHB2E.js?v=4ba18bce:798:5
    at update_reaction (chunk-O3RU4Q5E.js?v=4ba18bce:3983:18)
    at update_effect (chunk-O3RU4Q5E.js?v=4ba18bce:4122:21)
    at create_effect (chunk-O3RU4Q5E.js?v=4ba18bce:3466:7)
    at block (chunk-O3RU4Q5E.js?v=4ba18bce:3632:17)
    at Module.if_block (chunk-GMPPHB2E.js?v=4ba18bce:796:3)
    at +page.svelte?t=1774689309840:98:16
    at Module.add_svelte_meta (chunk-O3RU4Q5E.js?v=4ba18bce:600:12)
```

### System Info

```shell
SvelteLab, also reproduced locally with latest Svelte and SvelteKit
```

### Severity

annoyance


Discussion:
[REPL](https://svelte.dev/playground/hello-world?version=5.55.0#H4sIAAAAAAAAA21RwU7DMAz9FROQ2KRtvZcygUATJ8SBG-UQGpdGyuKSeCtT1X_HKaMMiVOcZ_v5PbtXXm9R5eoBnSPoKDgDMzSW0czVQtXWYVT5S6_40Ka6BAh-7Lpt21Xco-OEvemI_-EVeUbPQqOKWAXb8rr0DhkMXMOFwWD3aGa605bhKdDWRlwFjOT2OOvBaNY5lCq9pVpAo-P9CHHYIQzz-VXpsww2FCDSFiGgjuSBGxtBGzFiyWsHxzEgaMCPnQ0SM0ncBjK7Cr8VxYa6DXLV3LE-FWdWx7Hz0hfZZKL0_bmtwQwScn8jRiNDn5ROOgehGfOpIhX_4AkouWjXzw3Ct6Oxcyiydj1W5-gigrScqhqgOFsuRXeNAX2FYhQnc5FAdmgIo79keBdD3ATqPMhyD7BcrqehIx146v5O-1X1SOPmp3Rma0mOj5yU8ZNVni4wvMpPW9dZb1Re60TyBcVdY1JVAgAA)



Patch:
diff --git a/.changeset/common-flowers-listen.md b/.changeset/common-flowers-listen.md
new file mode 100644
index 000000000000..8d021b8054cc
--- /dev/null
+++ b/.changeset/common-flowers-listen.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: correctly calculate `@const` blockers
diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts
index 9d24f9dbac0b..7941113a7f14 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts
+++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts
@@ -2,6 +2,7 @@ import type { Scope } from '../scope.js';
 import type { ComponentAnalysis, ReactiveStatement } from '../types.js';
 import type { AST, StateField, ValidatedCompileOptions } from '#compiler';
 import type { ExpressionMetadata } from '../nodes.js';
+import type { Identifier } from 'estree';
 
 export interface AnalysisState {
 	scope: Scope;
@@ -33,6 +34,13 @@ export interface AnalysisState {
 	 * Set when we're inside a `$derived(...)` expression (but not `$derived.by(...)`) or `@const`
 	 */
 	derived_function_depth: number;
+
+	/** Collected info about async `{@const }` declarations */
+	async_consts?: {
+		id: Identifier;
+		/** How many `@const` declarations there are (already) in this scope */
+		declaration_count: number;
+	};
 }
 
 export type Context<State extends AnalysisState = AnalysisState> = import('zimmerframe').Context<
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js
index 77ea6549054b..22ffc24c1ea9 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js
@@ -1,6 +1,7 @@
 /** @import { AST } from '#compiler' */
 /** @import { Context } from '../types' */
 import * as e from '../../../errors.js';
+import * as b from '#compiler/builders';
 import { validate_opening_tag } from './shared/utils.js';
 
 /**
@@ -42,4 +43,28 @@ export function ConstTag(node, context) {
 		function_depth: context.state.function_depth + 1,
 		derived_function_depth: context.state.function_depth + 1
 	});
+
+	const has_await = node.metadata.expression.has_await;
+	const blockers = [...node.metadata.expression.dependencies]
+		.map((dep) => dep.blocker)
+		.filter((b) => b !== null && b.object !== context.state.async_consts?.id);
+
+	if (has_await || context.state.async_consts || blockers.length > 0) {
+		const run = (context.state.async_consts ??= {
+			id: context.state.analysis.root.unique('promises'),
+			declaration_count: 0
+		});
+		node.metadata.promises_id = run.id;
+
+		const bindings = context.state.scope.get_bindings(declaration);
+
+		// keep the counter in sync with the number of thunks pushed in ConstTag in transform
+		// TODO 6.0 once non-async and non-runes mode is gone investigate making this more robust
+		// via something like the approach in https://github.com/sveltejs/svelte/pull/18032
+		const length = run.declaration_count++;
+		const blocker = b.member(run.id, b.literal(length), true);
+		for (const binding of bindings) {
+			binding.blocker = blocker;
+		}
+	}
 }
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js
index 02d780dc0dc7..def38601758f 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js
@@ -6,5 +6,5 @@
  * @param {Context} context
  */
 export function Fragment(node, context) {
-	context.next({ ...context.state, fragment: node });
+	context.next({ ...context.state, fragment: node, async_consts: undefined });
 }
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js
index d2bd3c10dcd6..ac8f120d3d3d 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js
@@ -1,7 +1,6 @@
-/** @import { Expression, Identifier, Pattern } from 'estree' */
+/** @import { Expression, Identifier, Pattern, Statement } from 'estree' */
 /** @import { AST } from '#compiler' */
 /** @import { ComponentContext } from '../types' */
-/** @import { ExpressionMetadata } from '../../../nodes.js' */
 import { dev } from '../../../../state.js';
 import { extract_identifiers } from '../../../../utils/ast.js';
 import * as b from '#compiler/builders';
@@ -27,13 +26,7 @@ export function ConstTag(node, context) {
 
 		context.state.transform[declaration.id.name] = { read: get_value };
 
-		add_const_declaration(
-			context.state,
-			declaration.id,
-			expression,
-			node.metadata.expression,
-			context.state.scope.get_bindings(declaration)
-		);
+		add_const_declaration(context.state, declaration.id, expression, node.metadata);
 	} else {
 		const identifiers = extract_identifiers(declaration.id);
 		const tmp = b.id(context.state.scope.generate('computed_const'));
@@ -70,13 +63,7 @@ export function ConstTag(node, context) {
 			expression = b.call('$.tag', expression, b.literal('[@const]'));
 		}
 
-		add_const_declaration(
-			context.state,
-			tmp,
-			expression,
-			node.metadata.expression,
-			context.state.scope.get_bindings(declaration)
-		);
+		add_const_declaration(context.state, tmp, expression, node.metadata);
 
 		for (const node of identifiers) {
 			context.state.transform[node.name] = {
@@ -90,42 +77,40 @@ export function ConstTag(node, context) {
  * @param {ComponentContext['state']} state
  * @param {Identifier} id
  * @param {Expression} expression
- * @param {ExpressionMetadata} metadata
- * @param {import('#compiler').Binding[]} bindings
+ * @param {AST.ConstTag['metadata']} metadata
  */
-function add_const_declaration(state, id, expression, metadata, bindings) {
+function add_const_declaration(state, id, expression, metadata) {
 	// we need to eagerly evaluate the expression in order to hit any
 	// 'Cannot access x before initialization' errors
 	const after = dev ? [b.stmt(b.call('$.get', id))] : [];
 
-	const has_await = metadata.has_await;
-	const blockers = [...metadata.dependencies]
+	const blockers = [...metadata.expression.dependencies]
 		.map((dep) => dep.blocker)
 		.filter((b) => b !== null && b.object !== state.async_consts?.id);
 
-	if (has_await || state.async_consts || blockers.length > 0) {
+	if (metadata.promises_id) {
 		const run = (state.async_consts ??= {
-			id: b.id(state.scope.generate('promises')),
+			id: metadata.promises_id,
 			thunks: []
 		});
 
 		state.consts.push(b.let(id));
 
-		const assignment = b.assignment('=', id, expression);
-		const body = after.length === 0 ? assignment : b.block([b.stmt(assignment), ...after]);
+		/** @type {Statement | undefined} */
+		let promise_stmt;
 
 		if (blockers.length === 1) {
-			run.thunks.push(b.thunk(b.member(/** @type {Expression} */ (blockers[0]), 'promise')));
+			promise_stmt = b.stmt(b.await(b.member(/** @type {Expression} */ (blockers[0]), 'promise')));
 		} else if (blockers.length > 0) {
-			run.thunks.push(b.thunk(b.call('$.wait', b.array(blockers))));
+			promise_stmt = b.stmt(b.await(b.call('$.wait', b.array(blockers))));
 		}
 
-		run.thunks.push(b.thunk(body, has_await));
-
-		const blocker = b.member(run.id, b.literal(run.thunks.length - 1), true);
-
-		for (const binding of bindings) {
-			binding.blocker = blocker;
+		// keep the number of thunks pushed in sync with ConstTag in analysis phase
+		const assignment = b.assignment('=', id, expression);
+		if (promise_stmt) {
+			run.thunks.push(b.thunk(b.block([promise_stmt, b.stmt(assignment)]), true));
+		} else {
+			run.thunks.push(b.thunk(assignment, metadata.expression.has_await));
 		}
 	} else {
 		state.consts.push(b.const(id, expression));
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ConstTag.js
index d2ff9a10b4bb..87d60442f632 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ConstTag.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ConstTag.js
@@ -1,4 +1,4 @@
-/** @import { Expression, Pattern } from 'estree' */
+/** @import { Expression, Pattern, Statement } from 'estree' */
 /** @import { AST } from '#compiler' */
 /** @import { ComponentContext } from '../types.js' */
 import * as b from '#compiler/builders';
@@ -12,36 +12,37 @@ export function ConstTag(node, context) {
 	const declaration = node.declaration.declarations[0];
 	const id = /** @type {Pattern} */ (context.visit(declaration.id));
 	const init = /** @type {Expression} */ (context.visit(declaration.init));
-	const has_await = node.metadata.expression.has_await;
 	const blockers = [...node.metadata.expression.dependencies]
 		.map((dep) => dep.blocker)
 		.filter((b) => b !== null && b.object !== context.state.async_consts?.id);
 
-	if (has_await || context.state.async_consts || blockers.length > 0) {
+	if (node.metadata.promises_id) {
 		const run = (context.state.async_consts ??= {
-			id: b.id(context.state.scope.generate('promises')),
+			id: node.metadata.promises_id,
 			thunks: []
 		});
 
 		const identifiers = extract_identifiers(declaration.id);
-		const bindings = context.state.scope.get_bindings(declaration);
 
 		for (const identifier of identifiers) {
 			context.state.init.push(b.let(identifier.name));
 		}
 
+		/** @type {Statement | undefined} */
+		let promise_stmt;
+
 		if (blockers.length === 1) {
-			run.thunks.push(b.thunk(/** @type {Expression} */ (blockers[0])));
+			promise_stmt = b.stmt(b.await(/** @type {Expression} */ (blockers[0])));
 		} else if (blockers.length > 0) {
-			run.thunks.push(b.thunk(b.call('Promise.all', b.array(blockers))));
+			promise_stmt = b.stmt(b.await(b.call('Promise.all', b.array(blockers))));
 		}
 
+		// keep the number of thunks pushed in sync with ConstTag in analysis phase
 		const assignment = b.assignment('=', id, init);
-		run.thunks.push(b.thunk(b.block([b.stmt(assignment)]), has_await));
-
-		const blocker = b.member(run.id, b.literal(run.thunks.length - 1), true);
-		for (const binding of bindings) {
-			binding.blocker = blocker;
+		if (promise_stmt) {
+			run.thunks.push(b.thunk(b.block([promise_stmt, b.stmt(assignment)]), true));
+		} else {
+			run.thunks.push(b.thunk(assignment, node.metadata.expression.has_await));
 		}
 	} else {
 		context.state.init.push(b.const(id, init));
diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts
index 3c1e3e772c59..2964e467617e 100644
--- a/packages/svelte/src/compiler/types/template.d.ts
+++ b/packages/svelte/src/compiler/types/template.d.ts
@@ -155,6 +155,8 @@ export namespace AST {
 		/** @internal */
 		metadata: {
 			expression: ExpressionMetadata;
+			/** If this const tag contains an await expression, or needs to wait on other async, this is set */
+			promises_id?: Identifier;
 		};
 	}
 


Test patch:
diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-const-blocker/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-const-blocker/_config.js
new file mode 100644
index 000000000000..afa737f36c5e
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-derived-const-blocker/_config.js
@@ -0,0 +1,9 @@
+import { tick } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+	async test({ assert, target }) {
+		await tick();
+		assert.htmlEqual(target.innerHTML, '<p>data</p>');
+	}
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-const-blocker/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-const-blocker/main.svelte
new file mode 100644
index 000000000000..a15485968387
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-derived-const-blocker/main.svelte
@@ -0,0 +1,16 @@
+<script>
+let d = $derived(await Promise.resolve({ data: "data", hasData: true }));
+let showFetchCta = $derived(d.hasData)
+</script>
+
+{#if d}
+	{@const {data, hasData} = d}
+
+	{#if hasData}
+		<p>{data}</p>
+	{:else if showFetchCta}
+		<p>Fetch now</p>
+	{:else}
+		<p>No data</p>
+	{/if}
+{/if}
diff --git a/packages/svelte/tests/snapshot/samples/async-const/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-const/_expected/server/index.svelte.js
index 5f3cfa6ca638..c0cb999e8540 100644
--- a/packages/svelte/tests/snapshot/samples/async-const/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/async-const/_expected/server/index.svelte.js
@@ -7,16 +7,7 @@ export default function Async_const($$renderer) {
 
 		let a;
 		let b;
-
-		var promises = $$renderer.run([
-			async () => {
-				a = (await $.save(1))();
-			},
-
-			() => {
-				b = a + 1;
-			}
-		]);
+		var promises = $$renderer.run([async () => a = (await $.save(1))(), () => b = a + 1]);
 
 		$$renderer.push(`<p>`);
 		$$renderer.async([promises[1]], ($$renderer) => $$renderer.push(() => $.escape(b)));
diff --git a/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/server/index.svelte.js
index 3a5347594461..0cebc412ef56 100644
--- a/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/server/index.svelte.js
@@ -28,25 +28,15 @@ export default function Async_in_derived($$renderer, $$props) {
 			let no2;
 
 			var promises = $$renderer.run([
-				async () => {
-					yes1 = (await $.save(1))();
-				},
-
-				async () => {
-					yes2 = foo((await $.save(1))());
-				},
-
-				() => {
-					no1 = (async () => {
-						return await 1;
-					})();
-				},
-
-				() => {
-					no2 = (async () => {
-						return await 1;
-					})();
-				}
+				async () => yes1 = (await $.save(1))(),
+				async () => yes2 = foo((await $.save(1))()),
+				() => no1 = (async () => {
+					return await 1;
+				})(),
+
+				() => no2 = (async () => {
+					return await 1;
+				})()
 			]);
 		} else {
 			$$renderer.push('<!--[-1-->');