Skip to content

experimental.async: invariant_violation "Batch has scheduled roots" — $effect.pending() + {#each await …} with bind:this in a branch created after initial render #18675

Description

@Tyler-Petrov

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:

  1. a template read of $effect.pending()
  2. an {#each await promise as item} block
  3. a bind:this inside the each body
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions