JavaScript Maximum Call Stack Exceeded
Find unbounded recursion or circular traversal behind RangeError, then convert to iteration, add a base case, or break cycles with a visited set.
Symptoms
- RangeError: Maximum call stack size exceeded
- The tab freezes or crashes when rendering or parsing a nested structure
- The error appears only with deeply nested or self-referencing data
Possible causes
- A recursive function lacks a base case or never reaches it
- A recursive serializer or clone walks a circular object graph
- A getter or event handler triggers itself indirectly
- A recursive retry or scheduling loop runs without a yield or termination condition
Diagnosis steps
Read the stack trace and identify the repeating frame
DevTools Console → expand the RangeError stackInterpretation
The same function repeating dozens of times points to recursion. Note the input that triggered it.
Reproduce with a minimal nested or circular input
node -e "const a = {}; a.self = a; JSON.stringify(a)"Interpretation
A thrown TypeError about circular structure confirms a cycle; a RangeError confirms recursion depth.
Add a depth counter or visited set temporarily
grep -rn 'function .*(' src | head # locate recursive helpersInterpretation
Logging depth and object identity shows whether the input is deeper than expected or cyclic.
Root cause
Each function call consumes a stack frame. When recursion depth or mutual recursion exceeds the engine's stack limit, JavaScript throws RangeError. Circular data or a missing termination condition makes the depth unbounded.
Fix
Add a base case that is guaranteed to be reached, convert deep recursion to an explicit stack or loop, and track visited objects with WeakSet when traversing graphs. For asynchronous loops, yield between iterations instead of recursing through promises.
Risk note
Catching RangeError and continuing can leave partially processed data. Increasing the stack size only postpones the failure and is not portable across engines.
Verify the fix
Add a regression test with deep nesting and a cyclic object. Confirm no RangeError, verify the traversal terminates, and check that the result is correct. Run the same test in the target browser and Node.js versions.
Stack overflow is a termination bug
The engine limits how many frames can be active. Recursion is fine when depth is bounded and the base case is reachable. It fails when the input is deeper than expected, the base case is missing, or the structure contains a cycle.
// ❌ No cycle protection
function clone(value) {
if (typeof value !== 'object' || value === null) return value;
const out = Array.isArray(value) ? [] : {};
for (const key of Object.keys(value)) out[key] = clone(value[key]);
return out;
}
const a = {};
a.self = a;
clone(a); // RangeError
// ✅ Track visited objects
function clone(value, seen = new WeakMap()) {
if (typeof value !== 'object' || value === null) return value;
if (seen.has(value)) return seen.get(value);
const out = Array.isArray(value) ? [] : {};
seen.set(value, out);
for (const key of Object.keys(value)) out[key] = clone(value[key], seen);
return out;
}
Other fixes
- Convert tail-like recursion to a loop or an explicit stack.
- Add a depth limit and return a clear error for untrusted input.
- Yield between asynchronous iterations with
awaitand a timer instead of recursive promise chains. - Break cycles in graph traversals with a
WeakSet.
Verify
- The minimal reproducer no longer throws.
- A cyclic object is handled explicitly.
- Deeply nested input terminates within the expected depth.
- A regression test covers the original input in the target runtime.
Increasing the stack size can make the failure less frequent but does not fix an unbounded traversal.