From 82069153ff4bb359910a73289d09203f448a1302 Mon Sep 17 00:00:00 2001 From: Nathan Nguyen Date: Fri, 19 Dec 2025 20:16:42 -0500 Subject: [PATCH] fix: prevent RangeError from infinite loop in each block reconciliation Add cycle detection to prevent infinite loops in the each block's linked list traversal during reconciliation. If a cycle is detected in the linked list (where a node's `next` pointer references an already-visited node), the loop breaks early to prevent the `to_destroy` array from growing indefinitely and causing a RangeError. In development mode, a warning is logged to help with debugging when a cycle is detected. Fixes #17368 --- .../src/internal/client/dom/blocks/each.js | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index f7c9faf2b1d2..a0108d5386ca 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -493,6 +493,13 @@ function reconcile(state, array, anchor, flags, get_key) { stashed = []; while (current !== null && current !== effect) { + // Detect cycle in linked list to prevent infinite loop + if (seen !== undefined && seen.has(current)) { + if (DEV) { + console.warn('Svelte: cycle detected in each block linked list, breaking to prevent infinite loop'); + } + break; + } (seen ??= new Set()).add(current); stashed.push(current); current = current.next; @@ -536,7 +543,21 @@ function reconcile(state, array, anchor, flags, get_key) { } } + // Track visited nodes to detect cycles in the linked list + // A cycle would cause an infinite loop and eventually a RangeError + /** @type {Set} */ + var visited = new Set(); + while (current !== null) { + // Detect cycle in linked list to prevent infinite loop + if (visited.has(current)) { + if (DEV) { + console.warn('Svelte: cycle detected in each block linked list, breaking to prevent infinite loop'); + } + break; + } + visited.add(current); + // If the each block isn't inert, then inert effects are currently outroing and will be removed once the transition is finished if ((current.f & INERT) === 0 && current !== state.fallback) { to_destroy.push(current);