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            // Node

So 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 ... crash

Every 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 — crash

Use 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 forever

The 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 JSON

JSON.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);   // safe

Note 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.

Frequently Asked Questions

What does maximum call stack size exceeded mean? Too many function calls accumulated without returning, exhausting the fixed-size call stack. It is JavaScript's stack overflow, and it nearly always means recursion that does not terminate or a call chain that is genuinely too deep.
Why does it happen in React with no recursive function? Usually state is being updated during render, so each render triggers another. Move state updates into event handlers or effects, and check that any effect setting state does not also list that state in its dependency array.
How do I fix recursion that is too deep for valid input? Rewrite it iteratively with an explicit stack or queue, which uses heap memory instead of the call stack and can handle far larger inputs. JavaScript engines generally do not optimise tail calls, so restructuring alone will not help.
Why does spreading a large array cause this error? Spread passes each element as a separate function argument, and arguments occupy stack space. With around a hundred thousand elements the frame becomes too large. Use reduce or a loop instead of Math.max(...arr).
What causes converting circular structure to JSON? An object that references itself directly or through a chain, so serialisation would never finish. JSON.stringify detects it and throws a clear error, while hand-written recursive clones usually blow the stack instead. Use structuredClone for cyclic data.