From a6002b587c9d2f40fe2dc48391c372360abbaf0f Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 21 May 2026 20:26:47 +0200 Subject: [PATCH] fix: unlink errored and otherwise finished batch (#18264) This fixes the issue in https://github.com/sveltejs/svelte/issues/18221#issuecomment-4497918414 where an error can create follup-up invariant errors. The batch errors, has no chance to run otherwise (no more pending work) and is therefore "dead". That means we need to unlink it otherwise it's becoming a "zombie" and hangs around, causing unnecessary and potentially buggy (as seen in the reproduction) merge/commit work. I was not able to reduce the reproduction down to a test case that fails without the fix, but it does make a related error test from #17888 work more correctly. --- .changeset/beige-bobcats-eat.md | 5 +++++ packages/svelte/src/internal/client/reactivity/batch.js | 6 ++++++ .../tests/runtime-runes/samples/error-recovery/_config.js | 6 +++--- 3 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 .changeset/beige-bobcats-eat.md diff --git a/.changeset/beige-bobcats-eat.md b/.changeset/beige-bobcats-eat.md new file mode 100644 index 0000000000..95390c3c2e --- /dev/null +++ b/.changeset/beige-bobcats-eat.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: unlink errored and otherwise finished batch diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 4952001dd6..b9721b6243 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -326,6 +326,12 @@ export class Batch { this.#traverse(root, effects, render_effects); } catch (e) { reset_all(root); + // If there's no async work left, this branch is now dead and needs + // to be unlinked to not become a zombie that is never cleaned up. + // See https://github.com/sveltejs/svelte/issues/18221#issuecomment-4497918414 + // for a (non-minimal) reproduction that demonstrates a case where this is necessary + // to not get follow-up false-positives via "batch has scheduled roots" invariant errors. + if (!this.#is_deferred()) this.#unlink(); throw e; } } diff --git a/packages/svelte/tests/runtime-runes/samples/error-recovery/_config.js b/packages/svelte/tests/runtime-runes/samples/error-recovery/_config.js index 52c1bbd1bf..1f80251806 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-recovery/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/error-recovery/_config.js @@ -2,7 +2,7 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; export default test({ - async test({ assert, target, compileOptions }) { + async test({ assert, target }) { const [toggle, increment] = target.querySelectorAll('button'); flushSync(() => increment.click()); @@ -25,8 +25,8 @@ export default test({ ` -

show: ${compileOptions.experimental?.async ? 'false' : 'true'}

- ` +

show: true

+ ` // show: false would also be fine; this is more about ensuring that things continue to work _somehow_ ); } });