What you'll learn
Quick Answer
This is a stack overflow: too many function calls piled up without returning. The usual cause is recursion with no base case or one that is never reached, but it also appears with circular object references, event handlers that trigger themselves, React components updating state during render, and spreading a very large array into a function. Fix the termination condition, or convert the recursion into an iterative loop.
What the Call Stack Is
Every function call pushes a frame onto the call stack holding its arguments, local variables and the return address. The frame is popped when the function returns.
The stack has a fixed size — typically around ten thousand frames in a browser, though it varies. Exceed it and you get:
RangeError: Maximum call stack size exceeded // Chrome
InternalError: too much recursion // Firefox
RangeError: Maximum call stack size exceeded // NodeSo the error always means the same thing: functions kept calling without returning.
The simplest case is obvious once seen:
function countdown(n) {
console.log(n);
countdown(n - 1); // nothing stops it
}
countdown(5); // 5, 4, 3, 2, 1, 0, -1, -2 ... crashEvery recursive function needs a base case that returns without recursing, and every path must move toward it.
function countdown(n) {
if (n < 0) return; // base case
console.log(n);
countdown(n - 1); // moves toward it
}
The Base Case That Is Never Reached
Harder to spot: a base case exists but the recursion steps past it.
function countdown(n) {
if (n === 0) return; // only stops at exactly 0
countdown(n - 2); // from an odd number, jumps 1 → -1 → -3 ...
}
countdown(5); // never equals 0 — crashUse an inequality rather than equality whenever the step size is not one:
if (n <= 0) return;The same failure appears with floating point, where exact equality is unreliable:
function shrink(x) {
if (x === 0) return; // 0.1 - 0.05 repeatedly never lands exactly on 0
shrink(x - 0.05);
}And with tree or graph traversal that revisits nodes:
function walk(node) {
for (const child of node.children) walk(child);
}
// If a child points back at an ancestor, this recurses foreverThe fix there is a visited set — the same defence that prevents infinite loops in graph search.
Accidental Recursion
These cause the most confusion, because the code contains no obvious recursive call.
A setter that assigns to itself.
class User {
set name(value) {
this.name = value; // calls the setter again — infinite
}
}Use a differently named backing field, conventionally with an underscore or a # private field.
An event handler that triggers its own event.
input.addEventListener('change', () => {
input.value = format(input.value);
input.dispatchEvent(new Event('change')); // fires itself forever
});A toString that logs itself.
class Point {
toString() { return `Point ${this}`; } // template literal calls toString
}React state updated during render. The most common version in modern code:
function Counter() {
const [n, setN] = useState(0);
setN(n + 1); // render → state change → render → ...
return <p>{n}</p>;
}State updates belong in an effect or an event handler, never in the render body. The same happens with a useEffect that sets a state value it also depends on — the dependency array then retriggers it endlessly.
Circular References
Any operation that walks an object graph will loop forever if the graph contains a cycle.
const a = { name: 'a' };
const b = { name: 'b' };
a.friend = b;
b.friend = a; // circular
JSON.stringify(a);
// TypeError: Converting circular structure to JSONJSON.stringify detects this specifically and gives a clearer error. Hand-written deep clones and comparisons usually do not, and blow the stack instead.
function deepClone(obj) {
const out = {};
for (const k in obj) {
out[k] = typeof obj[k] === 'object' ? deepClone(obj[k]) : obj[k];
}
return out; // stack overflow on circular data
}Use structuredClone, which handles cycles correctly, or track visited objects in a WeakMap if you must write your own.
This appears constantly with DOM nodes, since element.parentNode.children points back, and with ORM models where a parent references children that reference the parent. Logging such an object in a naive serialiser is a reliable way to hang a process.
When the Recursion Is Correct but Too Deep
Sometimes there is no bug — the input is simply large. Recursing over a linked list of 50,000 nodes is legitimate logic that still exceeds the stack.
Convert to iteration with an explicit stack. Any recursive algorithm can be rewritten this way, and the heap is far larger than the call stack.
// Recursive DFS — crashes on deep graphs
function dfs(node) {
visit(node);
node.children.forEach(dfs);
}
// Iterative — bounded only by memory
function dfs(root) {
const stack = [root];
while (stack.length) {
const node = stack.pop();
visit(node);
stack.push(...node.children);
}
}Process large arrays in chunks rather than spreading them into a call. Spread passes each element as a separate argument, and a large enough array overflows the stack on its own:
Math.max(...hugeArray); // RangeError with ~100k+ elements
hugeArray.reduce((a, b) => Math.max(a, b), -Infinity); // safeNote that JavaScript engines generally do not implement tail-call optimisation despite it being specified, so rewriting a function to be tail-recursive does not save you. Iteration is the reliable answer.
