AniUI Academy

Recursion vs. Iteration, and Stack Overflow Risk

Why a recursive function that works perfectly in every test can crash in production on deeply nested real-world data — and the explicit-stack technique that removes the risk entirely.

9 min read

The previous lesson's sum function traced cleanly by hand at four levels deep. Real frontend data doesn't stay four levels deep — a comment thread, a deeply nested JSON config, a recursively-defined UI tree — and this lesson is about the specific, real failure mode that shows up when it doesn't, and the one technique that removes it entirely.

The risk, concretely

function countNodes(node) {
  if (!node) return 0;
  return 1 + node.children.reduce((sum, child) => sum + countNodes(child), 0);
}

This is completely correct, and it will pass every test you write with reasonable-looking nested data. It will also crash with RangeError: Maximum call stack size exceeded the day someone's data is nested deep enough — and "deep enough" is smaller than people expect. V8's default stack depth allows on the order of ten to fifteen thousand simple frames before overflowing (the exact number depends on frame size and engine version, and isn't something to hard-code a assumption around) — which sounds like a lot, until you remember that user-generated content (a deeply threaded reply chain, a maliciously or accidentally deep JSON payload) has no obligation to respect what your test fixtures assumed was "deep."

This is precisely the space-complexity point from several lessons back, made concrete: every unreturned recursive call is a real stack frame, and the stack has a hard, finite size that your test suite's shallow fixtures never exercise.

The tail-call myth, addressed directly

A tempting fix: "just write it as a tail call, and the engine will optimize away the stack growth." This is in the ECMAScript specification — proper tail calls — and it's a real, standardized feature.

It is also not implemented by V8 (Chrome, Edge, Node) or SpiderMonkey (Firefox). Only Safari's JavaScriptCore shipped it. So a recursive function written in tail position gets zero stack-space benefit in the large majority of real deployment targets — this isn't a nuance, it's the practical answer to "can I rely on this," and the answer is no, for any code that needs to run outside Safari specifically.

The real fix: an explicit stack

The technique that actually removes the risk: replace the implicit stack (the engine's call stack, fixed-size, not sized for your program) with an explicit one — a regular JavaScript array, living on the heap, limited only by available memory rather than a fixed engine ceiling.

function countNodesIterative(root) {
  if (!root) return 0;
 
  let count = 0;
  const stack = [root]; // an explicit stack, replacing the call stack
 
  while (stack.length > 0) {
    const node = stack.pop();
    count++;
    for (const child of node.children) {
      stack.push(child);
    }
  }
 
  return count;
}

Compare this directly to the recursive version. Same logic — visit a node, count it, queue up its children to visit next — but "queue up" now means pushing onto a real array instead of making a real function call. Same O(n) time (every node is still visited exactly once). The difference is entirely in space: the "how much is in progress at once" bookkeeping now lives in a structure with a much larger practical limit, not the engine's reserved stack space.

  1. Step 1

    Recursive version

    Each 'visit a child' is a real function call, adding a frame to the engine's fixed-size call stack.

  2. Step 2

    Risk

    Deep-enough real data exceeds the engine's stack limit — a RangeError, not a bug in your logic.

  3. Step 3

    Iterative version

    Each 'visit a child' pushes onto a plain array on the heap instead of calling a function.

  4. Step 4

    Result

    Same O(n) time and the same total nodes tracked, but bounded by available memory, not a fixed stack depth.

Recursive call stack versus explicit array stack — same traversal, different memory ceiling.

When recursion is still the right call

None of this means "never recurse." A function recursing over a genuinely bounded, small structure — a fixed-depth config schema, a known-shallow category tree with a hard maximum of a few levels by the product's own design — never gets anywhere near a stack limit, and there the recursive version's clarity (it reads as "the definition," not "a workaround") is a clean win with no real downside. The judgment call is specifically about unbounded or user/data-controlled depth: comment threads, arbitrary JSON, file systems, anything shaped by content you don't control.

Try it yourself
Loading playground...

What to remember

  • Recursive functions that work perfectly in every test can still crash in production, because real user data can be nested far deeper than test fixtures ever are — the risk is about depth you don't control, not about correctness.
  • Tail-call optimization is not a safety net in Chrome, Edge, Firefox, or Node — only Safari implements it, so don't rely on it to prevent overflow.
  • Rewriting a recursive traversal with an explicit array-based stack keeps the same O(n) time but moves the "in progress" bookkeeping off the fixed- size call stack and onto the heap, removing the overflow risk.
  • Recursion is still the right, clear choice when depth is genuinely small and bounded by the problem's own structure — the risk is specifically about unbounded or user-influenced depth.

Check yourself

4 questions · pass 3/4 to unlock Memoization, and a Taste of Dynamic Programming

up to 50
  1. 1.A recursive function that walks a nested comments tree works fine in every test (nesting depth under 20) but throws "Maximum call stack size exceeded" in production on one specific thread. What is the most likely explanation?

  2. 2.Does JavaScript (as run by V8 in Chrome, Edge, and Node, or SpiderMonkey in Firefox) apply tail-call optimization to remove stack frames for a recursive call written in tail position?

  3. 3.Converting a recursive tree-walk into an iterative one using an explicit array as a stack changes what, compared to the recursive version, for a tree that fits in memory either way?

  4. 4.When is recursion still the right choice over an explicit-stack iterative rewrite, despite the stack-overflow risk discussed in this lesson?

4 left to answer