Fix "Maximum Call Stack Size Exceeded" in JavaScript
RangeError: Maximum call stack size exceeded means your code has infinite or excessively deep recursion. Every function call adds a frame to the call stack — when the stack fills up (around 10,000–15,000 frames in V8), JavaScript throws this error. This guide covers every cause with working fixes: infinite recursion, circular object references, deep recursion on large data, mutual recursion, React infinite render loops, and how to convert recursion to iteration for any algorithm.
~10,000
max call stack depth in V8 (Chrome/Node.js)
Recursion
primary cause — missing base case or infinite loop
Iterative
the fix — convert recursion to a loop with explicit stack
Circular ref
JSON.stringify on circular objects also triggers this
The exact error in each browser/runtime
What Is the Call Stack?
Every function call in JavaScript creates a new "stack frame" containing the function's local variables, parameters, and the return address. These frames are pushed onto the call stack. When the function returns, its frame is popped. The stack has a fixed size determined by the JavaScript engine — V8 allows around 10,000–15,000 frames.
The stack frame model
Every function call pushes a "frame" onto the call stack containing local variables, parameters, and the return address. When the function returns, the frame is popped. The stack has a fixed size (~10,000–15,000 frames in V8). Filling it → RangeError. Recursive functions that don't eventually hit a base case never pop their frames.
Cause 1 — Infinite Recursion (Missing Base Case)
The most common cause. A recursive function must always have a base case — a condition that stops the recursion. Without it, the function calls itself indefinitely until the stack overflows.
Always include a base case in recursive functions
No base case — infinite recursion
// ❌ No base case → infinite recursion → stack overflow
function countdown(n) {
console.log(n);
countdown(n - 1); // calls itself with no stop condition
// Stack grows: countdown(10) → countdown(9) → ... → RangeError
}Base case added — stops at n=0
// ✅ Base case prevents infinite recursion
function countdown(n) {
if (n <= 0) return; // base case: stop when n reaches 0
console.log(n);
countdown(n - 1); // recursive case: guaranteed to reach n=0
}
// Check your base case logic:
// 1. Does the base case condition ever become true?
// 2. Does each recursive call move CLOSER to the base case?
// 3. What happens if the initial value is already at the base case?
countdown(5); // logs: 5, 4, 3, 2, 1
countdown(0); // returns immediately — base case on first callCause 2 — Circular Object References in JSON.stringify
JSON.stringify internally uses recursion to walk the object tree. If an object references itself (directly or indirectly), JSON.stringify recurses infinitely and crashes the stack.
Handle circular references before stringifying
Circular reference crashes JSON.stringify
const obj = { name: 'Alice', items: [] };
obj.parent = obj; // circular: obj references itself
JSON.stringify(obj); // ❌ RangeError: Maximum call stack size exceededDetect and skip circular refs before stringifying
// Option A: Remove the circular reference (cleanest)
const obj = { name: 'Alice', items: [] };
// Don't add: obj.parent = obj
// Option B: Use a WeakSet replacer to detect and skip circular refs
function stringifyCircularSafe(obj) {
const seen = new WeakSet();
return JSON.stringify(obj, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) return '[Circular Reference]';
seen.add(value);
}
return value;
});
}
// Option C: Use the 'flatted' or 'circular-json' npm package for full round-trip support
import { stringify, parse } from 'flatted';
const json = stringify(circularObj); // handles circular refs with special encoding
const restored = parse(json); // restores original structureCause 3 — Deep Recursion on Large Data
Even with a correct base case, recursing over 100,000 elements will overflow the stack. The ~10,000 frame limit means any recursion depth beyond that will crash. The fix: convert recursion to an iterative loop.
Convert deep recursion to iteration
Recursive — overflows at depth ~10k
// ❌ Recursive sum — overflows on large arrays
function sum(arr, i = 0) {
if (i >= arr.length) return 0;
return arr[i] + sum(arr, i + 1); // 100k frames → RangeError
}
sum(new Array(100000).fill(1)); // crashesIterative — no stack limit, works on any size array
// ✅ Iterative — O(1) stack space, no limit
function sum(arr) {
let total = 0;
for (const n of arr) total += n; // no stack frames allocated
return total;
}
sum(new Array(100000).fill(1)); // works fine → 100000
// ✅ Also works: Array methods (reduce, forEach) — internally iterative
const total = new Array(100000).fill(1).reduce((acc, n) => acc + n, 0);Cause 4 — Mutual Recursion (Two Functions Calling Each Other)
Replace mutual recursion with simple computation
Mutual recursion — 100k frames for n=100000
// ❌ A calls B, B calls A — infinite mutual recursion
function isEven(n) {
if (n === 0) return true;
return isOdd(n - 1); // calls isOdd
}
function isOdd(n) {
if (n === 0) return false;
return isEven(n - 1); // calls isEven back
}
isEven(100000); // 100k frames → RangeErrorModulo operation — O(1), no recursion
// ✅ Simple modulo — O(1), no recursion at all
function isEven(n) { return Math.abs(n) % 2 === 0; }
function isOdd(n) { return Math.abs(n) % 2 !== 0; }
isEven(100000); // → true, instantCause 5 — React Infinite Re-render Loop
Never call setState during render
setState in render body → infinite loop
// ❌ setState called during render → triggers re-render → setState again...
function Counter() {
const [count, setCount] = useState(0);
setCount(count + 1); // ❌ called directly in render body — not in event/effect
return <div>{count}</div>;
}setState only in event handlers or useEffect
// ✅ setState only in event handlers or useEffect
function Counter() {
const [count, setCount] = useState(0);
// ✅ Only update on explicit user interaction
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
);
}
// ✅ useEffect with correct dependencies to avoid infinite loop
function DataLoader({ userId }) {
const [data, setData] = useState(null);
useEffect(() => {
fetchUser(userId).then(setData);
}, [userId]); // ✅ only re-run when userId changes, not on every render
return <div>{data?.name}</div>;
}Converting Tree Recursion to Iteration
Tree traversal is the most common algorithm that needs to be made iterative. Replace the call stack with an explicit array (stack) that you manage yourself.
// ❌ Recursive tree traversal — overflows on deep trees
function sumRecursive(node) {
if (!node) return 0;
return node.value + sumRecursive(node.left) + sumRecursive(node.right);
// A tree with 10k nodes can cause overflow if tree is unbalanced (linear chain)
}
// ✅ Iterative tree traversal — uses explicit stack array, no call stack limit
function sumIterative(root) {
if (!root) return 0;
let total = 0;
const stack = [root]; // explicit stack — manages traversal ourselves
while (stack.length > 0) {
const node = stack.pop(); // process current node
total += node.value;
if (node.right) stack.push(node.right); // push children for later
if (node.left) stack.push(node.left);
}
return total;
}
// ✅ Iterative DFS for graph traversal
function dfs(graph, startNode) {
const visited = new Set();
const stack = [startNode];
const result = [];
while (stack.length > 0) {
const node = stack.pop();
if (visited.has(node)) continue;
visited.add(node);
result.push(node);
for (const neighbor of graph[node]) {
if (!visited.has(neighbor)) stack.push(neighbor);
}
}
return result;
}
// ✅ General recursion → iteration using explicit call stack
// When you have: function f(state) { ... f(newState) ... }
// Convert to:
function fIterative(initialState) {
const stack = [initialState];
const results = [];
while (stack.length > 0) {
const state = stack.pop();
// ... process state ...
// Instead of calling f(newState), push to stack:
stack.push(newState);
}
return results;
}Diagnosing Which Function Is Looping
Read the stack trace
The error message includes a stack trace. Look for a function name that repeats dozens or hundreds of times: "at functionName (file.js:42)" repeated many times. That's your infinite recursion.
Use Chrome DevTools
Open DevTools → Sources → enable "Pause on exceptions" → trigger the error. DevTools pauses at the exact crash point. The Call Stack panel shows the full stack — you'll see the repeating function frames.
Add a recursion depth counter
Temporarily add a depth parameter: function f(n, depth = 0) { if (depth > 100) { console.trace("deep recursion at depth", depth); throw new Error("too deep"); } ... f(n-1, depth+1); }. This gives you the depth and a trace when it gets unexpectedly deep.
Check for circular dependencies in React components
If the error is in a React app, look for: setState called during render, useEffect with missing or incorrect dependencies causing infinite re-fetching, or two components that each trigger the other's re-render.