Tail Recursion

Tail Position Enables TCO — V8 Skips It in Practice, Use an Explicit Stack

Tail Recursion

Tail-call optimization eliminates stack frames when the recursive call is the very last operation, but V8 does not apply TCO outside strict-mode edge cases, so converting to an explicit stack loop is the pragmatic solution.

5 min read Level 3/5 #dsa#recursion#tail-recursion
What you'll learn
  • Define tail position and distinguish tail calls from non-tail calls
  • Explain why V8 does not reliably apply tail-call optimization in practice
  • Convert a recursive function to an iterative one using an explicit stack

A function call is in tail position when it is the very last operation before a function returns — nothing else happens with its result. When a recursive call is in tail position, the current frame is no longer needed and could theoretically be replaced rather than stacked, keeping memory usage constant regardless of depth. This optimization is called tail-call optimization (TCO).

Tail Call vs. Non-Tail Call

// NOT a tail call — we still multiply by n after the recursive call returns
function factorial(n) {
  if (n === 0) return 1;
  return n * factorial(n - 1); // n * (...) is the last op, not the call itself
}

// Tail-call version — accumulator carries the running product
function factorialTail(n, acc = 1) {
  if (n === 0) return acc;
  return factorialTail(n - 1, n * acc); // recursive call IS the last op
}

In factorialTail, after calling itself there is nothing left to do — the result of the recursive call is the result of the current call. That is the defining property of a tail call.

V8 and TCO: The Reality

The ES2015 specification requires TCO in strict mode. V8 implemented it briefly in 2016 (behind a flag in Node 6) but later removed it from the default path due to performance profiling difficulties and debugging complications (frames that no longer exist cannot be inspected). As of 2026, V8 does not apply TCO in practical Node.js code, even for properly written tail calls.

The bottom line: writing a tail-recursive function in JavaScript does not save stack space in V8. You will still hit RangeError: Maximum call stack size exceeded at large inputs.

Converting to Iteration with an Explicit Stack

The production-safe approach is to simulate the call stack with a plain array:

// Iterative DFS using an explicit stack (replaces recursive tree traversal)
function dfsIterative(root) {
  if (root === null) return [];
  const result = [];
  const stack = [root];

  while (stack.length > 0) {
    const node = stack.pop();
    result.push(node.value);

    // Push right first so left is processed first (LIFO)
    if (node.right !== null) stack.push(node.right);
    if (node.left !== null) stack.push(node.left);
  }

  return result;
}

This has the same logic as a recursive pre-order traversal but uses O(h) heap memory (where h is tree height) instead of O(h) stack frames — and heap memory is not capped at ~10 000 slots.

When Recursion Is Still Fine

ScenarioMax depthUse recursion?
Tree height in balanced BST~17 for 100kYes
Merge sort on 10k elements~14Yes
Flat array of 100k elements100 000No — use loop
Graph DFS on social networkPotentially 1MNo — use stack

Recursion is elegant and safe for naturally bounded depths. Switch to an explicit stack or loop when depth scales with input size.

Up Next

See how divide-and-conquer — arguably the most powerful recursive pattern — splits problems into sub-problems and combines their results efficiently.

Divide and Conquer →