How to Fix Maximum Call Stack Size Exceeded in JavaScript
Diagnose and fix the RangeError: Maximum call stack size exceeded across recursion, circular JSON, React renders, and getter loops.
How to Fix "Maximum Call Stack Size Exceeded" in JavaScript
RangeError: Maximum call stack size exceeded means your program pushed more frames onto the call stack than the JavaScript engine allows. Modern V8 (Chrome, Node) tops out around 10,000–15,000 frames depending on frame size, Firefox’s SpiderMonkey allows roughly 25,000–40,000, and Safari’s JavaScriptCore sits near 40,000. The limit is not part of the spec, so a script that runs on one browser can still crash on another. The fix is almost always to break a runaway call chain, not to raise the limit.
Step 1: Read the stack trace and find the repeating frame
Open DevTools, reproduce the error, and expand the stack. The offending function is the one that appears over and over near the bottom of the trace. In Node, run with node --stack-trace-limit=100 script.js to see more frames. If the trace shows the same function name repeating, it is direct recursion; if two names alternate, it is mutual recursion; if the frames are inside JSON.stringify, util.inspect, or React.render, jump to the matching section below.
Step 2: Fix infinite recursion (missing or unreachable base case)
The classic cause is a recursive function whose base case never fires. Check the terminating condition and the argument you pass on the recursive call — they must move toward the base case.
// Broken: n never reaches 0 because we pass n, not n - 1
function countdown(n) {
if (n === 0) return;
console.log(n);
countdown(n); // bug
}
// Fixed
function countdown(n) {
if (n <= 0) return;
console.log(n);
countdown(n - 1);
}Also guard against non-integer or negative inputs (countdown(0.5) would recurse forever with n === 0 as the sole base case). Prefer n <= 0.
Step 3: Break mutual recursion cycles
When a() calls b() which calls a(), the base case has to live in one of them and actually stop the chain. Trace the flow on paper. A common fix is to pass a depth counter and bail out, or to restructure so one function returns data instead of calling back.
Step 4: Handle circular references before serializing or logging
If the trace ends in JSON.stringify, an object references itself — often a parent/child DOM-like graph or a Redux state that stores a node twice. Use a replacer that tracks seen objects:
const seen = new WeakSet();
JSON.stringify(obj, (k, v) => {
if (typeof v === 'object' && v !== null) {
if (seen.has(v)) return '[Circular]';
seen.add(v);
}
return v;
});For debug logging, prefer console.dir(obj, { depth: 3 }) in Node or the DevTools object inspector, which both truncate cycles.
Step 5: Stop React infinite render loops
Calling setState (or a state setter from useState) during render schedules another render, which runs the setter again. Fix by moving the update into an event handler or useEffect with a proper dependency array.
// Broken: setCount runs every render
function Counter() {
const [count, setCount] = useState(0);
setCount(count + 1); // triggers re-render, infinite loop
return <div>{count}</div>;
}
// Fixed: run once on mount
useEffect(() => { setCount(c => c + 1); }, []);The same trap hits useEffect with a missing dep array, or a dep that is a new object/array literal each render. Memoize with useMemo/useCallback or move the value outside the component.
Step 6: Untangle getters and setters that call each other
A getter that reads its own property, or two accessors that reference each other, will recurse forever. Store the underlying value in a different key (convention: leading underscore, or a WeakMap) and read from that inside the accessor.
// Broken
get name() { return this.name; }
// Fixed
get name() { return this._name; }
set name(v) { this._name = v; }Step 7: When recursion depth is legitimate, use trampolining or iteration
If the algorithm genuinely needs deep recursion (tree walks over millions of nodes, deep JSON), rewrite it iteratively with an explicit stack, or trampoline it — return a thunk instead of recursing, and drive it in a loop:
function trampoline(fn) {
return (...args) => {
let result = fn(...args);
while (typeof result === 'function') result = result();
return result;
};
}
const sum = trampoline(function s(n, acc = 0) {
return n === 0 ? acc : () => s(n - 1, acc + n);
});
sum(1_000_000); // no stack overflowNote that JavaScript engines outside Safari do not implement proper tail-call optimization, so a plain tail-recursive rewrite will not save you — you must trampoline or iterate.