Every function call pushes an execution context onto the call stack, and the stack is a fixed-size region of memory chosen by the host. If calls nest deeper than it allows (endless recursion, or legitimate recursion over a big input), the engine throws: RangeError: Maximum call stack size exceeded in V8 and JavaScriptCore, InternalError: too much recursion in Firefox. The depth is engine-, platform-, and frame-size-dependent; the spec defines neither the limit nor the error type. A trivial function reaches about ten thousand frames in Node 24, and it can be changed with --stack-size, which is a workaround rather than a fix.
Proper tail calls. ES2015 specifies that a call in tail position (a return f(x) whose result is returned as is) in strict code must not grow the stack: PrepareForTailCall discards the current execution context before the call. That would make deep tail recursion run in constant stack space.
But in practice, no. Only JavaScriptCore (Safari) implements it. V8 doesn't (a tail-recursive loop overflows in Node, and V8's flag list has no tail-call option), and no other major engine ships it either. The TC39 follow-up that would make it an explicit opt-in (return continue f(x), "syntactic tail calls") has been inactive since 2016. So you can't rely on tail-call elimination in portable JavaScript, and code that needs deep recursion should avoid depending on it.
What to do instead:
- Iterate. Turn the recursion into a loop.
- Use an explicit stack (an array) for trees and graphs.
- Trampoline: return a thunk instead of calling, and let a loop run the thunks.
- Break the stack with the event loop for long async work, since each task starts on an empty stack.