Tail Call Optimization Explained
Tail call optimization reuses a function's stack frame for its final call instead of pushing a new one, turning some recursion into constant stack space.
Tail call optimization (TCO) is a compiler or runtime technique that reuses the current function’s stack frame for a call that happens as the very last action in that function, instead of pushing a new frame on top. When it applies, a recursive function that would otherwise grow the call stack with every recursive step can run in constant stack space — no matter how many times it recurses.
What makes a call a “tail call”
A call is in tail position when it’s the final thing a function does before returning — its return value is returned directly, with no further computation applied to it afterward.
function factorial(n) {
return n * factorial(n - 1); // NOT a tail call — multiplication happens after the call returns
}
function factorialTail(n, acc = 1) {
if (n <= 1) return acc;
return factorialTail(n - 1, n * acc); // tail call — nothing left to do after it returns
}
The first version isn’t a tail call because the multiplication happens after factorial(n - 1) returns — the current frame needs to stay alive to hold n until that multiplication completes. The second version restructures the same logic using an accumulator parameter, so the recursive call is genuinely the last operation — there’s nothing left for the current frame to do once it returns.
Why the stack frame can be reused
Every function call normally pushes a new stack frame holding local variables, the return address, and saved registers. When a call is in tail position, the calling frame has no more work to do after the callee returns — its own return value is the callee’s return value. That means the runtime doesn’t need to keep the caller’s frame around at all: it can overwrite the current frame with the callee’s arguments and jump, rather than push a new frame and later pop back through it.
This is why TCO matters for recursion vs iteration: a properly tail-recursive function, when TCO’d, uses the same constant stack space as an equivalent loop — the recursive elegance of the code doesn’t cost anything at runtime. Without TCO, deep recursion risks a stack overflow, since each call adds a frame and the stack has a fixed size.
Which languages actually do this
Support is inconsistent, and it’s a common source of surprise for people coming from a language that guarantees it. Scheme’s language specification mandates proper tail calls — it’s part of the language semantics, not an optional optimization. Functional languages like Erlang, Elixir, and Haskell rely on it heavily, since idiomatic code in those languages leans on recursion rather than mutable loops.
JavaScript’s ECMAScript 2015 specification included “proper tail calls” as a required feature, but adoption among engines has been inconsistent — Safari’s JavaScriptCore implements it, while V8 (Chrome, Node.js) and SpiderMonkey (Firefox) have not shipped it, largely over concerns about debuggability, since TCO makes stack traces harder to reconstruct. Practically, this means you should never assume tail-recursive JavaScript is stack-safe across all engines — check the specific runtime, or restructure deep recursion into an explicit loop.
Many compiled languages perform TCO as a compiler optimization rather than a semantic guarantee: it happens when the optimizer can prove it’s safe and beneficial, but the language doesn’t promise it will always happen, so relying on it for correctness (rather than as a performance nicety) is fragile unless the language spec explicitly commits to it.
Rewriting recursion to be tail-recursive
The factorial example above shows the general technique: introduce an accumulator parameter that carries the partial result forward, so each recursive call can be the last operation instead of a computation that needs the result of a deeper call first. Many recursive algorithms — summing a list, reversing a list, computing a running total — can be rewritten this way. Algorithms that inherently need to combine two separate recursive results, like a naive tree traversal that recurses both left and right, don’t fit the pattern as cleanly, since there’s genuinely more work to do after at least one of the calls returns.
When it doesn’t help
TCO only optimizes calls that are actually in tail position. A function with a try/finally wrapped around the recursive call isn’t a tail call, because the finally block still has to run after the call returns. Non-tail recursive algorithms — including many classic divide-and-conquer approaches — don’t benefit from TCO at all, and in languages without it, they eventually need either an explicit loop with a manual stack, or acceptance of the stack depth limit.
The takeaway
Tail call optimization reuses a stack frame for a call that’s the last action in a function, turning otherwise stack-growing recursion into constant-space execution — effectively as cheap as a loop. It’s a language-and-runtime-dependent feature, not something you can assume works everywhere: Scheme guarantees it, JavaScript’s spec allows it but most engines don’t implement it, and plenty of compiled languages treat it as an optimizer’s best effort. When you need guaranteed stack safety for deep recursion, check whether your specific runtime actually performs TCO before relying on it.
Keep reading
Takina · · 4 min read NFA vs DFA: How Regex Engines Actually Work
Most regex engines backtrack through an NFA; a few compile to a DFA instead. The difference explains why some patterns hang forever and others never do.
The Lycoris Team · · 4 min read The Rope Data Structure: Editing Huge Strings Efficiently
A rope is a binary tree of string chunks that makes inserting, deleting, and slicing huge strings fast, which is why text editors use it over plain arrays.
The Lycoris Team · · 4 min read Memoization vs. Tabulation in Dynamic Programming
Memoization caches results top-down via recursion; tabulation builds a table bottom-up with loops. Same technique, opposite direction, different tradeoffs.