From 667896a753031b5d58157feb4b4eb7df49222ac0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 12 Mar 2026 07:22:42 -0400 Subject: [PATCH] fix: recover from errors that leave a corrupted effect tree (#17888) https://github.com/sveltejs/svelte/pull/17680#issuecomment-3888440736. Errors that occur during traversal (not inside a template effect etc) can leave dirty effects inside the effect tree, but with clean parents. This means that a) subsequent changes to their dependencies won't schedule them to re-run b) subsequent batch flushes won't 'reach' them unless a sibling effect happens to be made dirty The easiest way to fix this is to just repair the tree if traversal fails. If you had a truly ginormous tree this could conceivably take a noticeable amount of time, but that's probably better than the app just being broken. Note that this doesn't apply to errors that occur inside an error boundary, because in that case the offending subtree gets destroyed. This is just for errors that bubble all the way to the root. Closes #17680, closes #17679. --- .changeset/public-plants-win.md | 5 +++ .../src/internal/client/reactivity/batch.js | 21 +++++++++++- .../samples/error-recovery/_config.js | 32 +++++++++++++++++++ .../samples/error-recovery/main.svelte | 13 ++++++++ 4 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 .changeset/public-plants-win.md create mode 100644 packages/svelte/tests/runtime-runes/samples/error-recovery/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/error-recovery/main.svelte diff --git a/.changeset/public-plants-win.md b/.changeset/public-plants-win.md new file mode 100644 index 0000000000..af65137426 --- /dev/null +++ b/.changeset/public-plants-win.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: recover from errors that leave a corrupted effect tree diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index cb115994f3..a09654bfc0 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -225,7 +225,12 @@ export class Batch { var updates = (legacy_updates = []); for (const root of roots) { - this.#traverse(root, effects, render_effects); + try { + this.#traverse(root, effects, render_effects); + } catch (e) { + reset_all(root); + throw e; + } } // any writes should take effect in a subsequent batch @@ -959,6 +964,20 @@ function reset_branch(effect, tracked) { } } +/** + * Mark an entire effect tree clean following an error + * @param {Effect} effect + */ +function reset_all(effect) { + set_signal_status(effect, CLEAN); + + var e = effect.first; + while (e !== null) { + reset_all(e); + e = e.next; + } +} + /** * Creates a 'fork', in which state changes are evaluated but not applied to the DOM. * This is useful for speculatively loading data (for example) when you suspect that diff --git a/packages/svelte/tests/runtime-runes/samples/error-recovery/_config.js b/packages/svelte/tests/runtime-runes/samples/error-recovery/_config.js new file mode 100644 index 0000000000..52c1bbd1bf --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-recovery/_config.js @@ -0,0 +1,32 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, compileOptions }) { + const [toggle, increment] = target.querySelectorAll('button'); + + flushSync(() => increment.click()); + assert.htmlEqual( + target.innerHTML, + ` + + +

show: false

+ ` + ); + + assert.throws(() => { + flushSync(() => toggle.click()); + }, /NonExistent is not defined/); + + flushSync(() => increment.click()); + assert.htmlEqual( + target.innerHTML, + ` + + +

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

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-recovery/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-recovery/main.svelte new file mode 100644 index 0000000000..03bfae2596 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-recovery/main.svelte @@ -0,0 +1,13 @@ + + + + + +

show: {show}

+ +{#if show} + +{/if}