JAVASCRIPT / FUNCTIONS
Recursion and the call stack limit
Size a recursion's depth against the engine's fixed call stack, read a stack overflow RangeError, and rewrite depth-bound recursion as a loop.
What you will learn
- Trace how many stack frames a recursive call leaves waiting to finish
- Read 'Maximum call stack size exceeded' as a depth problem, not a logic bug
- Rewrite linear recursion as a loop when depth grows with the input size
- Separate recursion depth from total call count when estimating cost
Understanding Recursion and the call stack limit
A call does not simply jump into a function body; it pushes a frame onto the call stack holding the arguments, the local variables, and the position to resume at when the call returns. In return n + sumTo(n - 1) the addition cannot run until the inner call hands back a value, so the outer frame has to stay put with its half-finished expression. Call sumTo(3000) and there is an instant when 3000 frames are stacked up, each parked on one pending +. Recursion is not a special mechanism to the engine, it is this ordinary stacking applied to the same function over and over.
The stack is a fixed-size block of memory reserved when the thread starts, so that stacking has a hard ceiling. The budget is counted in bytes rather than in calls, which is why the maximum depth is not a number you can look up: a function with eight parameters and several locals needs a fatter frame and runs out sooner than a one-argument function, and the figure differs between V8, SpiderMonkey, and JavaScriptCore. When the next frame will not fit, the call itself throws, reporting RangeError: Maximum call stack size exceeded in Chrome and Node, and InternalError: too much recursion in Firefox.
The repair is almost never to ask for a bigger stack, it is to stop leaving work unfinished. A loop with a running total keeps exactly one frame alive no matter how large the input gets, and moving the recursive call into tail position does not help, because V8 and SpiderMonkey never shipped the proper tail calls that ES2015 described. Recursion stays the right tool when depth follows the shape of the data rather than its size, since walking nested JSON or a DOM subtree goes only as deep as the structure is tall, a few dozen frames, however many nodes it contains.
function sumTo(n) {
if (n === 0) return 0; // base case: the only way out
return n + sumTo(n - 1); // the + must wait, so this frame stays alive
}
console.log(sumTo(5));
console.log(sumTo(3000));
try {
sumTo(1000000);
} catch (err) {
console.log('stopped:', err.name);
}The call stack holds one frame for every call that has not returned yet, so the fixed stack limits recursion depth, not the total number of calls.
Worked examples
Same sum, two memory profiles
Shows that a recursion whose depth equals the array length overflows on input a loop handles without effort.
const big = new Array(200000).fill(1);
function sumRec(arr, i = 0) {
if (i === arr.length) return 0;
return arr[i] + sumRec(arr, i + 1);
}
function sumLoop(arr) {
let total = 0;
for (const n of arr) total += n;
return total;
}
try {
sumRec(big);
} catch (err) {
console.log('recursive:', err.name);
}
console.log('loop:', sumLoop(big));Example explained
Line 1return arr[i] + sumRec(arr, i + 1) leaves an addition pending, so frame i cannot be popped until every later frame has returned.
Line 2Depth here equals arr.length, so 200000 elements demand 200000 live frames and the call throws before a single + is evaluated.
Line 3sumLoop reuses one frame and one total variable, so its stack use is the same for 200000 elements as for two.
Line 4Rewriting sumRec in tail position would fail at the same depth, since V8 still allocates a frame for every call.
Many calls, shallow stack
Separates total call count from live depth by instrumenting naive Fibonacci.
let calls = 0;
let depth = 0;
let maxDepth = 0;
function fib(n) {
calls++;
depth++;
if (depth > maxDepth) maxDepth = depth;
const result = n < 2 ? n : fib(n - 1) + fib(n - 2);
depth--;
return result;
}
console.log(fib(25));
console.log('calls:', calls, 'maxDepth:', maxDepth);Example explained
Line 1depth++ and depth-- bracket the body, so depth is exactly how many fib frames sit on the stack at that moment.
Line 2fib(n - 1) unwinds completely before fib(n - 2) is called, so the two branches never add their depths together.
Line 3242785 calls fit inside 25 frames because each frame is released the instant it returns.
Line 4fib(50) would still need only 50 frames but roughly 4e10 calls, so slowness and stack overflow are different failures.
Important notes
There is no portable maximum depth; it varies with engine, platform, and how many parameters and locals each frame stores, so never hard-code a number you consider safe.
Inside the catch block the stack is still nearly full, so a handler that calls anything substantial can overflow a second time.
Common mistakes
Stepping over the base case: if (n === 0) combined with n - 2 never matches an odd starting value, so n runs on to -1, -3, -5 and recursion continues until RangeError.
Assuming tail position fixes depth: return go(n - 1, total + n) reads like a loop, but Chrome, Node, and Firefox still push a frame per call and fail at the same depth.
Catching the RangeError and continuing: the caller gets undefined or a partial result, and the unbounded depth that caused the throw is still in the code.
Try it yourself
Change, predict, then run
In a browser console define function depth(n) { return n === 0 ? 0 : 1 + depth(n - 1); }, call it with 1000 and keep doubling inside try/catch until it throws, to find your engine's rough ceiling. Then rewrite the same computation as a while loop and confirm the input that threw now returns a number.
Open the JavaScript workspaceCheck your understanding
A function makes 500,000 recursive calls while producing its result, but never has more than 40 calls waiting to return at once. What happens?
- It completes, because the stack limits how many frames are live at once, not how many calls happen in total
- It throws RangeError, because 500,000 calls is far past the maximum call stack size
- It completes only because engines apply tail-call optimization to reuse the frames
- It throws in the browser but not in Node, which has no stack limit
Show answer
Each frame is popped as soon as its call returns, so only the 40 unfinished calls occupy stack space at any instant; the other 499,960 calls cost time, not stack. The second option confuses cumulative calls with depth, which is the usual source of this mistake. The third is wrong twice over: V8 does not implement tail calls, and a 40-frame-deep pattern needs no such help.