Diego Betto's Blog
Photo by Tine Ivanič on Unsplash

Diego Betto · September 11, 2026 · 5 min di lettura

"Maximum call stack size exceeded": when recursion goes out of control

What the call stack actually is, the two most common causes of a stack overflow in JavaScript, and how to convert deep recursion into an iterative version.

Condividi:XLinkedInFacebookWhatsApp

Your function runs fine on small inputs, then this shows up the moment you throw real data at it:

⛔ Error

RangeError: Maximum call stack size exceeded

Unlike most JavaScript errors, this one isn’t really about a specific value being wrong — it’s about how many function calls are stacked on top of each other at once. Understanding the call stack explains both causes and the fix.

What the call stack actually is

Every time a function calls another function, the engine pushes a new “frame” onto the call stack, tracking where to resume once that call returns. The stack has a fixed size limit (it varies by engine and available memory, but it’s finite). When you exceed it, you get this error instead of the process silently running out of memory.

function countDown(n) {
  console.log(n);
  return countDown(n - 1); // no base case — this never stops
}

countDown(5); // RangeError: Maximum call stack size exceeded

Each call to countDown stays on the stack waiting for the one it called to return — and since it never does, the stack grows until it hits the limit.

Cause #1: recursion without a proper base case

The example above is the textbook version, but it’s usually subtler in real code — an off-by-one in the termination condition, or a base case that’s technically present but never actually reached for certain inputs:

function factorial(n) {
  if (n === 0) return 1;
  return n * factorial(n - 1);
}

factorial(-5); // n never reaches 0, decrementing forever — same error

The fix is making sure the base case is reachable for every valid input, and validating inputs that would obviously never reach it:

function factorial(n) {
  if (n < 0) throw new RangeError("factorial is not defined for negative numbers");
  if (n === 0) return 1;
  return n * factorial(n - 1);
}

Cause #2: getters/setters that reference themselves

Less obvious, but a common source of confusing stack overflows:

class Product {
  get price() {
    return this.price; // reads the getter it's defined inside — infinite recursion
  }
}

This looks like a typo once you spot it, but it’s easy to introduce when refactoring a plain property into a computed one and forgetting to rename the backing field:

class Product {
  #price;

  get price() {
    return this.#price; // reads the private backing field instead
  }

  set price(value) {
    this.#price = value;
  }
}

Converting deep recursion to an iterative version

For legitimately deep recursion (traversing a large tree, processing a long linked list) where there’s no bug, just more depth than the stack allows, the fix is structural: replace the recursive calls with an explicit loop and your own stack (an array), instead of relying on the call stack.

// Recursive — can overflow on a long enough list
function sumList(node) {
  if (!node) return 0;
  return node.value + sumList(node.next);
}

// Iterative — constant stack depth regardless of list length
function sumList(node) {
  let sum = 0;
  while (node) {
    sum += node.value;
    node = node.next;
  }
  return sum;
}

For tree traversal, the equivalent trick is an explicit array acting as a stack, replacing the implicit call stack:

function sumTree(root) {
  let sum = 0;
  const stack = [root];

  while (stack.length > 0) {
    const node = stack.pop();
    if (!node) continue;
    sum += node.value;
    stack.push(node.left, node.right);
  }

  return sum;
}

💡 Consiglio

JavaScript doesn’t have guaranteed tail-call optimization in most engines (it’s part of the spec, but V8 — the engine behind Chrome and Node — has never implemented it), so don’t rely on “just make it tail-recursive” as a fix the way you might in some other languages. The iterative rewrite above is the reliable option.

Is it always a bug, or can it be a real memory limit?

Both exist, and it’s worth distinguishing them. A true logic bug (missing/unreachable base case) fails at a small, consistent depth every time. Legitimately deep recursion on unusually large input (a huge JSON tree, a very long linked list) can fail only occasionally, at whatever depth the current stack size allows — that’s not a bug in your recursion, it’s a structural mismatch between your algorithm and how much depth is actually available, and the iterative rewrite above is the right fix either way.

FAQ

❓ Is this the same as a memory leak?

No — a memory leak is unbounded growth of retained objects over time, usually across many operations, and doesn’t throw this specific error. A stack overflow is a single call chain growing too deep in one synchronous burst. They can both come from runaway recursion, but they’re different failure modes with different fixes.

❓ Can I increase the stack size instead of fixing the recursion?

In Node.js, yes, with the --stack-size flag — but it only raises the ceiling, it doesn’t fix an actual infinite recursion bug, and it’s not something you can do in a browser environment at all. Treat it as a last resort for genuinely deep (but finite) recursion, not a substitute for finding a missing base case.

❓ Does async/await avoid this problem?

Not directly — recursion inside an async function can still overflow the stack the same way, since each await doesn’t reset stack depth by itself. What it does change is that each microtask tick effectively gets a fresh stack, so recursion that yields via await between calls (rather than calling itself synchronously) sidesteps the issue as a side effect, not because async fixes recursion depth directly.

Conclusion

“Maximum call stack size exceeded” always means too many function calls were stacked synchronously — either from a base case that’s missing or unreachable, a self-referencing getter/setter, or recursion that’s structurally correct but deeper than the stack allows for large input. The first two are bugs to fix directly; the third calls for rewriting the recursive calls as an explicit loop with your own stack.

References

Condividi:XLinkedInFacebookWhatsApp
Diego Betto

Written by

Diego Betto

Co-Founder & CTO at PAPION. Senior full-stack engineer specializing in React, TypeScript, Node.js, and application security.