From 7671ddf958120e83f3ce40c23fc5a3f635455471 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Wed, 3 Jun 2026 11:54:29 +0200 Subject: [PATCH] fix: remove scheduled roots invariant The invariant has served us well to uncover a few bugs but its time is up. There are real cases where batches can be scheduled. E.g. an effect can be scheduled but the corresponding decrement only be scheduled in the next microtask. If there's a processing happening in-between, we run into a false positive invariant. This is what happens in #18363 Fixes #18363 --- .changeset/spicy-pots-talk.md | 5 +++++ .../svelte/src/internal/client/reactivity/batch.js | 10 ++++------ 2 files changed, 9 insertions(+), 6 deletions(-) create mode 100644 .changeset/spicy-pots-talk.md diff --git a/.changeset/spicy-pots-talk.md b/.changeset/spicy-pots-talk.md new file mode 100644 index 0000000000..6ceb128429 --- /dev/null +++ b/.changeset/spicy-pots-talk.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: remove scheduled roots invariant diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 7d14b80519..101294107a 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -702,11 +702,9 @@ export class Batch { batch.discard(); } } else if (sources.length > 0) { - // 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'); - } + // A batch can have scheduled effects but decrement_pending wasn't called yet + // (can happen after a microtask in e.g. async blocks) + const scheduled = batch.#roots.length > 0 || batch.#decrement_queued; // A batch was unskipped in a later batch -> tell prior batches to unskip it, too if (is_earlier) { @@ -761,7 +759,7 @@ export class Batch { // Only apply and traverse when we know we triggered async work with marking the effects // and know this won't run anyway right afterwards - if (batch.#roots.length > 0 && !batch.#decrement_queued) { + if (!scheduled && batch.#roots.length > 0) { batch.apply(); for (var root of batch.#roots) {