Repository: sveltejs/svelte (2025-08-11T16:23:38Z)

Issue:
Async Svelte breaking block effects in template (in SvelteKit?)
**Edit: I have some further findings in a [comment below](https://github.com/sveltejs/svelte/issues/16387#issuecomment-3105868571), please see them for a better picture of the issue**

### Describe the bug

Hi, I'm not sure sure if this is a Svelte or SvelteKit issue because I'm not really sure what's happening. I can move the issue if needed. However as far as I can tell async svelte is not expected to break SvelteKit applications unless using the features specifically called out as breaking (which I'm not).

When enabling the async svelte feature flag I am having an issue where the page component is trying to access state (the page data) after the state has been removed. This only seems to happen to state this is used inside of an `{#each}` block as far as I can tell.

When navigating between pages that reuse the same page component (e.g. changing a slug) there is no error.

The error received in the browser is (see reproduction below):

```
Uncaught TypeError: $$props.data.pikachu is undefined

  in <unknown>
  in +layout.svelte
  in root.svelte
    _page +page.svelte:18
    each_array each.js:151
    update_reaction runtime.js:300
    execute_derived deriveds.js:308
    update_derived deriveds.js:331
    is_dirty runtime.js:219
    traverse_effect_tree_fn batch.js:251
    process_fn batch.js:177
    flush_effects batch.js:338
    flush batch.js:307
    ensure batch.js:415
    ensure batch.js:409
    internal_set sources.js:176
    set sources.js:155
    set legacy-client.js:109
    <anonymous> legacy-client.js:147
    $set legacy-client.js:157
    navigate client.js:1595
    _start_router client.js:2383
    _start_router client.js:2274
    start client.js:339
    async* (index):195
    promise callback* (index):194
+page.svelte:18:30
```

### Reproduction

The repo contains the smallest reproduction I could find: https://github.com/dangodai/async-sveltekit-bug

It is a basic SvelteKit application and can be run with `npm install` and `npm run dev`. To trigger the error

1) Begin on `/`
2) Click on the "Page 2" link to navigate to `/page2` and observe the error in the console

Some notes:

* Beginning on `/page2` and navigating to `/` does not trigger the error
* If you uncomment the `#each` block from page2, navigating away from either page will trigger the error
* Disabling async svelte causes everything to work as expected
* Adding a `<svelte:boundary>` with a `pending` snippet does not fix the error (and I don't think is required since we aren't doing any async work so I didn't include it)

### Logs

```shell

```

### System Info

```shell
Not included below: I am testing in `Mozilla Firefox 139.0.1`

  System:
    OS: Linux 6.15 Arch Linux
    CPU: (12) x64 AMD Ryzen 5 2600 Six-Core Processor
    Memory: 4.52 GB / 15.54 GB
    Container: Yes
    Shell: 5.2.37 - /bin/bash
  Binaries:
    Node: 22.17.0 - ~/.nvm/versions/node/v22.17.0/bin/node
    Yarn: 1.22.22 - /usr/bin/yarn
    npm: 10.9.2 - ~/.nvm/versions/node/v22.17.0/bin/npm
  Browsers:
    Chromium: 137.0.7151.68
  npmPackages:
    svelte: ^5.36.2 => 5.36.2
```

### Severity

blocking an upgrade


Discussion:
Just a couple of notes:

1. I've been unable to reproduce this in pure Svelte (no Kit), but I'm also clueless to the internals of how kit works in order to accurately recreate what might be happening
2. Using SvelteKit from the remote functions PR (https://github.com/sveltejs/kit/pull/13986) which I would expect to work well with async svelte still causes the error

I'd be surprised if I'm not missing something or doing something wrong though since I don't see others bringing up this issue, because from my perspective this would cause 99% of SvelteKit apps to break (anything using load functions and `#each` blocks). 
It looks like what's happening is that with the experimental async features on, when you switch routes the template continues executing a moment longer, even after the data has changed in the script tag. I added some logging and compared with and without async, and then added a svelte:boundary to catch the error.

without async:
<img width="1000" height="106" alt="Image" src="https://github.com/user-attachments/assets/7ce8eaa2-dce0-4255-bd82-0d1afb5f0602" />

with async:
<img width="746" height="343" alt="Image" src="https://github.com/user-attachments/assets/86bedd8a-0ced-49e4-82cd-89782bc81e97" />

with svelte:boundary
<img width="1000" height="271" alt="Image" src="https://github.com/user-attachments/assets/7646d3cc-0b34-4ea5-ba3f-45086f6d7379" />

---

A few approaches:

- using <svelte:boundary> will help, as seen, and provides some extra features
- optional chaining or an #if block will protect against it, without the nice error catching the boundary gives you
- you might also consider using the same key for each page, and sending the pokemon name as a value

That's about all I can figure out :)
I have simplified my reproduction quite a bit and I have some further findings (https://github.com/dangodai/async-sveltekit-bug)

For some reason when SvelteKit navigates `each`, `if`, and `key` effects (at a minimum, there may be other cases too) re-run on the page being destroyed, but after the page data has been switched to the incoming page data (or perhaps as a result of).

The contents of the block doesn't really matter, but rather the value that the block is reading/tracking (sorry, I'm not sure of the right terminology here, but the value used inside of the curly braces).

This means any nested page data will cause an error unless the incoming page data matches the same shape, **but only inside of certain block effects** (in other words, you will end up accessing a property of undefined).

Notably:
- Accessing a nested property in a basic text expression is fine (e.g. `{foo.bar.baz}`
- Accessing a nested property in an `{#await}` block works, and using the promise result works even when nesting one of the otherwise bad blocks

I still haven't been able to reproduce this in Svelte without Kit.
[Minimal reproduction](https://svelte.dev/playground/8c80a036b7fd4210a5144531b4e9691b?version=5.36.16)
Are there any known workarounds for this?
I got lucky with optional access everywhere (`?.`). The types do not enforce it so it is easy to overlook...
> Are there any known workarounds for this?

An alternative to optional chaining everything is to wrap the contents of any affected components/pages in a `<svelte:boundary>`
> > Are there any known workarounds for this?
> 
> An alternative to optional chaining everything is to wrap the contents of any affected components/pages in a `<svelte:boundary>`

This didn't work for me, even wrapping the children in the layout.
> > > Are there any known workarounds for this?
> > 
> > 
> > An alternative to optional chaining everything is to wrap the contents of any affected components/pages in a `<svelte:boundary>`
> 
> This didn't work for me, even wrapping the children in the layout.

IIRC it worked for me by putting the boundary _inside_ of the crashing component. In the case of SvelteKit, that means inside of each `+page.svelte`. It's a bit cumbersome to add to every page but probably less so than optional chaining. 

I imagine if you have a LayoutLoad function passing props to a layout, you might need to wrap the `+layout.svelte` too but I haven't tested that case. 
This issue is blocking us from being able to try the new [Remote functions](https://svelte.dev/docs/kit/remote-functions) release.
Same issue with the following use case (error when navigating away from the page):
```ts
// +page.server.ts
export const load = async () => {
  return { users: [] }
}
```
```svelte
<!-- +page.svelte -->
<script lang="ts">
let { data }: PageProps = $props()

const users = $derived(data.users.map(u => u))
</script>

{#each users as user}
  <div>{user.name}</div>
{/each}
```



Patch:
diff --git a/.changeset/neat-files-do.md b/.changeset/neat-files-do.md
new file mode 100644
index 000000000000..ddba170c78d1
--- /dev/null
+++ b/.changeset/neat-files-do.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: skip effects inside dynamic component that is about to be destroyed
diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js
index f16da9c42703..2697722b3953 100644
--- a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js
+++ b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js
@@ -62,8 +62,10 @@ export function component(node, get_component, render_fn) {
 			if (defer) {
 				offscreen_fragment = document.createDocumentFragment();
 				offscreen_fragment.append((target = create_text()));
+				if (effect) {
+					/** @type {Batch} */ (current_batch).skipped_effects.add(effect);
+				}
 			}
-
 			pending_effect = branch(() => render_fn(target, component));
 		}
 


Test patch:
diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-component-props-update/Comp-1.svelte b/packages/svelte/tests/runtime-runes/samples/svelte-component-props-update/Comp-1.svelte
new file mode 100644
index 000000000000..fdafa27c3cd0
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/svelte-component-props-update/Comp-1.svelte
@@ -0,0 +1,7 @@
+<script>
+	let { data } = $props();
+</script>
+
+{#each data.obj.arr as i}
+	<p>{i}</p>
+{/each}
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-component-props-update/Comp-2.svelte b/packages/svelte/tests/runtime-runes/samples/svelte-component-props-update/Comp-2.svelte
new file mode 100644
index 000000000000..e345a7697c6e
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/svelte-component-props-update/Comp-2.svelte
@@ -0,0 +1,1 @@
+<p>Comp 2</p>
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-component-props-update/_config.js b/packages/svelte/tests/runtime-runes/samples/svelte-component-props-update/_config.js
new file mode 100644
index 000000000000..ff5ca12dbf75
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/svelte-component-props-update/_config.js
@@ -0,0 +1,11 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+	async test({ assert, target }) {
+		const [btn] = target.querySelectorAll('button');
+		btn.click();
+		flushSync();
+		assert.htmlEqual(target.innerHTML, `<button>Change</button> <p>Comp 2</p>`);
+	}
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-component-props-update/main.svelte b/packages/svelte/tests/runtime-runes/samples/svelte-component-props-update/main.svelte
new file mode 100644
index 000000000000..abd785fff376
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/svelte-component-props-update/main.svelte
@@ -0,0 +1,16 @@
+<script>
+	import Comp_1 from './Comp-1.svelte';
+	import Comp_2 from './Comp-2.svelte';
+
+	let Comp = $state.raw(Comp_1);
+	let data = $state.raw({ obj: { arr: [1, 2, 3] } });
+
+	function change() {
+		Comp = Comp_2;
+		data = {};
+	}
+</script>
+
+<button onclick={change}>Change</button>
+
+<Comp {data} />