Describe the bug
Under compilerOptions.experimental.async, Svelte throws an uncaught invariant_violation: "Batch has scheduled roots" when a branch created after the initial render (an {#if} toggled later — in our app, opening a dialog) contains all four of:
- a template read of
$effect.pending()
- an
{#each await promise as item} block
- a
bind:this inside the each body
- a second
await expression inside the each body
Removing any one of the four makes the error disappear (verified by ablation). An already-resolved Promise.resolve([1]) is enough — no timing race is needed and the repro is deterministic (100% of loads). The UI still renders correctly after the error.
Also observed:
- Toggling
show from a click handler instead of setTimeout reproduces as well.
- Rendering the same markup on initial mount (no
{#if}) does not reproduce.
- Wrapping the branch contents in their own
<svelte:boundary> inside the {#if} suppresses the error, but a pre-existing boundary around the {#if} (with a pending snippet) does not.
Originally hit in production code where a list component reads $effect.pending() for a debounced loading indicator while rendering {#each await query} rows that contain a component with a bound ref and a nested await — first seen behind SvelteKit + bits-ui + a custom thenable, all of which turned out to be red herrings.
Possibly related: #18363 (same invariant, different trigger).
Reproduction
Playground: https://svelte.dev/playground/untitled#H4sIAAAAAAAAA22PwW7CMBBEf2VlODhSlLSHXgJBol_QQ2-Yg3E2xJKxI-8GWln598oExKWnlWbm7ewm4fUFRSM-NZsBBk1AZsBucthBDIEJIo4xiFL01iGJ5pAE_44ZyYIonwv241jRFR1n7aQJ_9NN8IyeSTRiSybakXfKK3bIQEO4QQtrYs0oe-0Ii002CfnbXjBMLGUB7Q7kI8pxwqKEj7clZ4Inhi4Ygha-YrhYwioiBXdFeXg_LqlchW6j_LZ-HeDTyvb3C-acSWvsezRcjeg768-yyHI2VqjNAPqm7aNJU553SvH2NDEHDyfru4YHS21CN-8WU3GSL7CoHPozD0-yXtB7NtW5ZVY-1baflRelYPxh0eSH5-P8ByaNbxe0AQAA
<script>
let show = $state(false);
setTimeout(() => (show = true), 50);
const docs = Promise.resolve([1]);
let el;
</script>
{#if show}
{$effect.pending()}
{#each await docs as doc}
<button bind:this={el}>
{(await docs).length}
</button>
{/each}
{/if}
Load the page; the error is thrown ~50 ms later when show flips. Reproduces in headless and headed Chromium; also reproduces with plain Vite + @sveltejs/vite-plugin-svelte (no SvelteKit) with compilerOptions: { experimental: { async: true } } as the only non-default flag.
Logs
Svelte error: invariant_violation
An invariant violation occurred, meaning Svelte's internal assumptions were flawed. This is a bug in Svelte, not your app — please open an issue at https://github.com/sveltejs/svelte, citing the following message: "Batch has scheduled roots"
https://svelte.dev/e/invariant_violation
at invariant_violation (svelte/src/internal/client/errors.js)
at invariant (svelte/src/internal/client/reactivity/batch.js)
at #commit (Batch)
at #process (Batch)
at Batch.flush
at run_all
at run_micro_tasks
The failing guard is invariant(batch.#roots.length === 0, 'Batch has scheduled roots') in src/internal/client/reactivity/batch.js (#commit).
System Info
Reproduced on svelte 5.56.3 and 5.56.9, and on the svelte.dev playground as of 2026-08-19.
vite 8.0.16, @sveltejs/vite-plugin-svelte 7.1.2 and 7.3.0.
Chromium 145 (headless shell and headed), Linux.
Severity
annoyance
Describe the bug
Under
compilerOptions.experimental.async, Svelte throws an uncaughtinvariant_violation: "Batch has scheduled roots"when a branch created after the initial render (an{#if}toggled later — in our app, opening a dialog) contains all four of:$effect.pending(){#each await promise as item}blockbind:thisinside the each bodyawaitexpression inside the each bodyRemoving any one of the four makes the error disappear (verified by ablation). An already-resolved
Promise.resolve([1])is enough — no timing race is needed and the repro is deterministic (100% of loads). The UI still renders correctly after the error.Also observed:
showfrom a click handler instead ofsetTimeoutreproduces as well.{#if}) does not reproduce.<svelte:boundary>inside the{#if}suppresses the error, but a pre-existing boundary around the{#if}(with apendingsnippet) does not.Originally hit in production code where a list component reads
$effect.pending()for a debounced loading indicator while rendering{#each await query}rows that contain a component with a boundrefand a nestedawait— first seen behind SvelteKit + bits-ui + a custom thenable, all of which turned out to be red herrings.Possibly related: #18363 (same invariant, different trigger).
Reproduction
Playground: https://svelte.dev/playground/untitled#H4sIAAAAAAAAA22PwW7CMBBEf2VlODhSlLSHXgJBol_QQ2-Yg3E2xJKxI-8GWln598oExKWnlWbm7ewm4fUFRSM-NZsBBk1AZsBucthBDIEJIo4xiFL01iGJ5pAE_44ZyYIonwv241jRFR1n7aQJ_9NN8IyeSTRiSybakXfKK3bIQEO4QQtrYs0oe-0Ii002CfnbXjBMLGUB7Q7kI8pxwqKEj7clZ4Inhi4Ygha-YrhYwioiBXdFeXg_LqlchW6j_LZ-HeDTyvb3C-acSWvsezRcjeg768-yyHI2VqjNAPqm7aNJU553SvH2NDEHDyfru4YHS21CN-8WU3GSL7CoHPozD0-yXtB7NtW5ZVY-1baflRelYPxh0eSH5-P8ByaNbxe0AQAA
Load the page; the error is thrown ~50 ms later when
showflips. Reproduces in headless and headed Chromium; also reproduces with plain Vite +@sveltejs/vite-plugin-svelte(no SvelteKit) withcompilerOptions: { experimental: { async: true } }as the only non-default flag.Logs
The failing guard is
invariant(batch.#roots.length === 0, 'Batch has scheduled roots')insrc/internal/client/reactivity/batch.js(#commit).System Info
Severity
annoyance