diff --git a/.changeset/slimy-frogs-share.md b/.changeset/slimy-frogs-share.md
new file mode 100644
index 0000000000..27a393d564
--- /dev/null
+++ b/.changeset/slimy-frogs-share.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: avoid false-positive batch invariant error
diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js
index a5c9a51eec..ae4f5a6dac 100644
--- a/packages/svelte/src/internal/client/reactivity/batch.js
+++ b/packages/svelte/src/internal/client/reactivity/batch.js
@@ -679,7 +679,9 @@ export class Batch {
batch.discard();
}
} else if (sources.length > 0) {
- if (DEV) {
+ // The microtask queue can contain the batch already scheduled to run right
+ // after this one is finished, so throwing the invariant would be wrong here.
+ if (DEV && !batch.#decrement_queued) {
invariant(batch.#roots.length === 0, 'Batch has scheduled roots');
}
@@ -732,7 +734,8 @@ export class Batch {
}
// Only apply and traverse when we know we triggered async work with marking the effects
- if (batch.#roots.length > 0) {
+ // and know this won't run anyway right afterwards
+ if (batch.#roots.length > 0 && !batch.#decrement_queued) {
batch.apply();
for (var root of batch.#roots) {
diff --git a/packages/svelte/tests/runtime-runes/samples/async-pending-batch/_config.js b/packages/svelte/tests/runtime-runes/samples/async-pending-batch/_config.js
new file mode 100644
index 0000000000..e7359efa2d
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-pending-batch/_config.js
@@ -0,0 +1,16 @@
+import { tick } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ // This test mainly checks that we don't run into the 'Batch has scheduled roots' invariant wrongly.
+ // It is crafted such that two batches are scheduled to run in the same microtask, and the first
+ // tries to rebase the second.
+ async test({ assert, target }) {
+ await tick();
+ const [run] = target.querySelectorAll('button');
+
+ run.click();
+ await tick();
+ assert.htmlEqual(target.innerHTML, ' none none 0');
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/async-pending-batch/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-pending-batch/main.svelte
new file mode 100644
index 0000000000..19305cfb76
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-pending-batch/main.svelte
@@ -0,0 +1,8 @@
+
+
+
+
+{selectedId ?? "none"} {selectedOption ?? "none"} {$effect.pending()}