Preventing Stack Overflow in JavaScript Recursion

Deep recursion crashes the call stack. Learn how to avoid stack overflow with tail recursion, trampolines, iteration conversion, and practical depth limits.

6 min read

Every recursive call adds a frame to the call stack. The stack has a limit -- typically around 10,000 frames in modern engines. When recursion goes deeper than that, you get a stack overflow:

javascriptjavascript
function infinite(n) {
  return infinite(n + 1); // No base case
}
 
infinite(1);
// RangeError: Maximum call stack size exceeded

The error message is "Maximum call stack size exceeded" in V8 (Chrome, Node.js) or "too much recursion" in Firefox. Same problem: the stack ran out of room.

How Deep Can You Go?

Stack depth limits vary by engine and are not something you should depend on. A small recursive function that keeps counting until it fails gives a rough sense of the actual limit on your current setup:

javascriptjavascript
function measureDepth(n = 0) {
  try {
    return measureDepth(n + 1);
  } catch (e) {
    return n;
  }
}
 
console.log(measureDepth()); // ~10000 in V8, varies by environment

The exact number depends on the engine, available memory, and how much data each frame holds. The key takeaway: do not design code that depends on deep recursion working.

Fix 1: Convert Recursion to Iteration

The most reliable fix. Every recursive function can be rewritten with a loop, since the two forms are computationally equivalent, the recursive version just uses the call stack to track progress instead of a loop variable:

javascriptjavascript
// Recursive: risks stack overflow for large arrays
function sumRec(arr) {
  if (arr.length === 0) return 0;
  return arr[0] + sumRec(arr.slice(1));
}

The iterative version does the same work with a single loop variable instead of one stack frame per element, so it has no depth limit at all:

javascriptjavascript
function sumIter(arr) {
  let total = 0;
  for (const n of arr) {
    total += n;
  }
  return total;
}

For tree traversal, keep your own stack. Instead of letting the call stack track which node to visit next, an explicit array does the same job with no recursion at all:

javascriptjavascript
function flattenIter(nested) {
  const result = [];
  const stack = [...nested];
 
  while (stack.length > 0) {
    const item = stack.pop();
    if (Array.isArray(item)) stack.push(...item); // Push children onto the stack
    else result.push(item);
  }
 
  return result.reverse();
}

The while loop keeps popping items off the stack array until it is empty, pushing any nested arrays back on instead of recursing into them:

javascriptjavascript
console.log(flattenIter([1, [2, [3, 4], 5], 6]));
// [1, 2, 3, 4, 5, 6]

You manage the stack explicitly with an array. No call stack overflow, no matter how deeply nested the input is.

Fix 2: Trampolines

A trampoline turns recursion into iteration by having each "recursive" call return a function instead of calling it directly. The trampoline loop calls each returned function until there are no more:

javascriptjavascript
function trampoline(fn) {
  return function(...args) {
    let result = fn(...args);
    while (typeof result === "function") {
      result = result();
    }
    return result;
  };
}

A factorial written for trampolining looks almost identical to the normal recursive version, except the recursive call is wrapped in an arrow function instead of being called directly:

javascriptjavascript
function factorial(n, acc = 1) {
  if (n <= 1) return acc;
  // Instead of calling factorial directly, return a function that will call it
  return () => factorial(n - 1, n * acc);
}

Wrapping factorial in trampoline lets it handle inputs far larger than the call stack could ever support directly, since the loop inside trampoline replaces the chain of nested calls entirely:

javascriptjavascript
const safeFactorial = trampoline(factorial);
console.log(safeFactorial(5));   // 120
console.log(safeFactorial(20000)); // Works without stack overflow

Each step returns a thunk -- a function wrapping the next call. The trampoline calls thunks one at a time in a loop, using constant stack space. The trade-off is more complex code and a slight performance cost.

Fix 3: Chunk with setTimeout

For non-blocking recursion (like processing a large tree without freezing the UI), split the work into chunks. Each chunk processes a fixed batch of items, then schedules the next chunk instead of continuing immediately:

javascriptjavascript
function processLargeArray(items, callback, batchSize = 1000) {
  let index = 0;
 
  function processChunk() {
    const end = Math.min(index + batchSize, items.length);
    for (let i = index; i < end; i++) { /* Process items[i] */ }
    index = end;
 
    if (index < items.length) setTimeout(processChunk, 0); // Yield to the event loop
    else callback();
  }
 
  processChunk();
}

setTimeout(processChunk, 0) yields control back to the browser, allowing the UI to stay responsive between chunks. This is not about avoiding stack overflow directly -- the chunk size controls depth. It is about keeping the application responsive while processing large data.

Fix 4: Depth Guard

When recursion depth depends on external input, add an explicit limit. The inner parse function tracks how deep it has gone and throws before the real call stack limit is ever at risk:

javascriptjavascript
function safeParse(node, maxDepth = 1000) {
  function parse(node, depth) {
    if (depth > maxDepth) throw new Error(`Maximum depth ${maxDepth} exceeded`);
 
    if (node.children) {
      for (const child of node.children) parse(child, depth + 1);
    }
  }
 
  parse(node, 0);
}

The depth guard prevents malicious or malformed input from crashing your program. If the input is deeper than expected, you get a clear error instead of a stack overflow.

Fix 5: Tail Recursion (Limited Support)

A recursive call is in "tail position" when it is the last thing the function does before returning, with no pending operation like a multiplication waiting on its result:

javascriptjavascript
// Tail recursive: the recursive call is the last operation
function factorial(n, acc = 1) {
  if (n <= 1) return acc;
  return factorial(n - 1, n * acc);
}
 
// Not tail recursive: multiplication happens after the recursive call returns
function factorial(n) {
  if (n <= 1) return 1;
  return n * factorial(n - 1);
}

ES6 specifies proper tail calls (PTC), meaning tail-recursive functions should not grow the stack. However, only Safari's JavaScriptCore engine has implemented this reliably. V8 (Chrome, Node.js) does not fully support PTC. Do not depend on tail call optimization in production JavaScript.

In practice, the safest default is to reach for iteration or an explicit stack whenever recursion depth is unpredictable, and to save trampolines and depth guards for the cases where recursion is genuinely the clearer solution.

For recursion basics, see How to Use Recursion in JavaScript: Full Tutorial. For the call stack and execution context, see JavaScript Function Scope Local vs Global Scope.

Rune AI

Rune AI

Key Insights

  • Stack overflow happens when recursive calls exhaust the call stack, typically around 10,000 frames.
  • Convert recursion to iteration using an explicit stack (array) for the most reliable fix.
  • Trampolines flatten recursion by returning the next call instead of making it directly.
  • Split deep recursion with setTimeout to yield control back to the event loop.
  • Always include a depth guard when recursion depth depends on external input.
RunePowered by Rune AI

Frequently Asked Questions

What is the maximum call stack size in JavaScript?

It varies by engine and environment. Chrome V8 typically allows around 10,000-15,000 frames. Node.js defaults to around 10,000. You can check with a recursive counter, but never depend on a specific number.

Does JavaScript have tail call optimization?

Yes, as of ES6 the spec requires proper tail calls, but only Safari and JavaScriptCore have implemented it reliably. V8 (Chrome, Node.js) has limited support. You cannot depend on tail call optimization in production JavaScript.

What is the simplest fix for a stack overflow?

Convert the recursion to iteration. Almost every recursive function can be rewritten with a while loop and an explicit stack (an array). This is the most reliable, cross-environment fix.

Conclusion

Stack overflow is the cost of recursion without limits. The fix depends on the situation: convert to iteration for linear problems, use a trampoline for deeply nested recursion, split work into chunks with setTimeout for non-blocking execution, or set a depth guard for unpredictable input. JavaScript does not reliably optimize tail calls, so do not depend on tail recursion for production code.