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.
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
| Scenario | Max depth | Use recursion? |
|---|---|---|
| Tree height in balanced BST | ~17 for 100k | Yes |
| Merge sort on 10k elements | ~14 | Yes |
| Flat array of 100k elements | 100 000 | No — use loop |
| Graph DFS on social network | Potentially 1M | No — 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 →