JavaScript Recursion Limit: What Happens When Your Stack Runs Out

June 18, 202610 min read
dsaalgorithmsinterview-prepjavascript
TL;DR
  • V8 recursion limit: ~10,000–15,000 frames; SpiderMonkey ~25,000; JavaScriptCore up to 50,000 — no runtime API to query it
  • RangeError maximum call stack size exceeded: JavaScript's only signal, with no warning; arrives as a generic error not a clean limit message like Python's
  • TCO is dead in V8: ECMAScript 2015 specced proper tail calls, V8 never shipped it, Safari removed it in 2020 — tail-recursive style still overflows in Node.js
  • Primary fix: convert recursive DFS to iterative with an explicit stack on the heap — same algorithm, any depth, no overflow
  • Trampoline pattern: each recursive step returns a thunk instead of calling itself; preserves recursive structure at the cost of function allocation overhead
  • Interview tell: "What if the tree is very unbalanced?" means the interviewer wants you to name the stack limit, propose iterative conversion, and explain why TCO won't save you

You write a clean DFS. It passes every test case. The interviewer smiles and says, "what happens with a tree of 100,000 nodes?" You say it works fine. It does not.

RangeError: Maximum call stack size exceeded is JavaScript's way of saying your call stack is full and also possibly mocking you a little. Every function call pushes a frame onto the stack. When you recurse 10,000 levels deep, that's 10,000 frames sitting in memory, stacked like a very expensive Jenga tower. Eventually the engine runs out of allocated space and throws. The tower falls.

This is not a bug in your algorithm. The constraint is baked into the engine, and it shows up differently in JavaScript than in Python or Go. Understanding it is the difference between a solution that works and one that crashes on the exact input your interviewer chose.

How Deep Is JavaScript's Recursion Limit?

The limit is not a fixed number. It depends on the JavaScript engine, the platform, and how much stack space each frame consumes. JavaScript does not tell you what the limit is. It just waits.

In V8 (Chrome and Node.js), you typically get around 10,000 to 15,000 frames for a minimal recursive function. Firefox's SpiderMonkey runs to roughly 25,000. Safari's JavaScriptCore reaches 40,000 or higher. These numbers shift between engine versions and platform memory configurations.

// Works in most environments. Gets dangerously close to the limit in V8. function countDown(n) { return n === 0 ? 0 : countDown(n - 1); } countDown(14000); // may crash in Node.js; passing in an interview is a coin flip

The number is not portable and not guaranteed, so writing code that depends on a specific recursion depth is a mistake.

Adding local variables shrinks your budget fast. Each frame has to hold the function's parameters, local variables, return address, and some bookkeeping. A function with four locals uses more stack space per call than one with nothing but a return value.

function withLocals(n) { const a = n, b = n * 2, c = b + 1, d = c * 3; return n === 0 ? 0 : withLocals(n - 1); } // Crashes at a lower depth than countDown, even for the same n

The practical ceiling to carry into an interview: assume V8 gives you roughly 10,000 safe frames. If your recursion could go deeper than that on the expected input, you have a problem.

JavaScript gives you no API to query the limit at runtime. There is no getRecursionLimit(). Python at least has sys.getrecursionlimit() so you can see the wall before you hit it. JavaScript just lets you run full speed into the glass. The constraint exists and kills your code; it just does not announce itself until it does. Understanding what recursion depth actually measures helps calibrate how this plays out across different input shapes.

The TCO Story: Why You Can't Count On It

ECMAScript 2015 included proper tail calls (PTC) in the spec. The idea: if a function returns a recursive call in tail position (nothing left to do after the call returns), the engine reuses the current stack frame instead of pushing a new one. Infinite tail recursion, zero stack growth. Sounds great.

// Tail-recursive factorial: the recursive call is in tail position function factTail(n, acc = 1) { if (n <= 1) return acc; return factTail(n - 1, n * acc); // nothing happens after this returns } // This is NOT tail-recursive: multiplication happens after the call returns function factorial(n) { if (n <= 1) return 1; return n * factorial(n - 1); // n * (...) is computed after recursion returns }

In a compliant engine, factTail would never overflow. The problem is that almost no engine implemented it.

V8 explicitly rejected PTC. The team cited debugger reliability: if the engine reuses frames, stack traces lose information, and debugging recursive code becomes much harder. A fair point. Also: they just did not ship it. Node.js never shipped it. Firefox enabled PTC briefly in Nightly builds and removed it in 2017. Safari was the only major engine to ship proper tail calls, and WebKit removed the feature in 2020 due to maintenance costs and lack of uptake.

The ECMAScript committee wrote it into the standard. Browser vendors collectively looked at the standard, shrugged, and went home. Classic JavaScript ecosystem energy.

As of 2026, PTC is a dead letter in the V8 ecosystem. Writing tail-recursive JavaScript is still good style. It often makes iterative conversion mechanical. But it does not buy you stack safety in Node.js or Chrome.

The --stack-size flag in Node.js does exist:

node --stack-size=16384 script.js # roughly doubles the default stack

The default is 984 KB. You can bump it. This delays the crash; it is not a fix. The new limit is still finite. The flag does nothing in browser environments. It cannot be set from inside user code. It is the programming equivalent of getting a slightly bigger trash can instead of taking out the garbage.

Where This Hits You in an Interview

Tree and graph problems are where JavaScript's stack limit surfaces most visibly.

A balanced BST with 100,000 nodes has depth around 17. That's fine. A degenerate BST built from sorted input is a linked list 100,000 nodes deep. A recursive DFS on that crashes. LeetCode test cases often stay within balanced-tree territory, which is why solutions that would fail on pathological inputs pass on the judge and give you false confidence heading into the actual interview room.

// Clean. Crashes on a skewed tree with depth > ~15,000 in V8. function maxDepth(node) { if (!node) return 0; return 1 + Math.max(maxDepth(node.left), maxDepth(node.right)); }

Graph DFS has the same exposure. A chain of 20,000 unvisited nodes traversed recursively fills the stack before you finish.

The interviewer question to listen for: "What happens if the input is very large, or the tree is extremely unbalanced?" That question is a polite way of asking whether you know this constraint exists. The unpolite version is an interviewer who just silently adds a degenerate test case to the judge and watches what happens.

Three Fixes

Explicit Stack in the Heap

The most direct fix is converting recursive traversal to use an explicit stack data structure. The heap is orders of magnitude larger than the call stack, and it will not throw a RangeError when you push too many items onto it.

function maxDepth(root) { if (!root) return 0; const stack = [[root, 1]]; let maxD = 0; while (stack.length) { const [node, depth] = stack.pop(); maxD = Math.max(maxD, depth); if (node.left) stack.push([node.left, depth + 1]); if (node.right) stack.push([node.right, depth + 1]); } return maxD; }

Same algorithm, same visit order, no recursion. This handles any depth.

Recursive DFS fills the call stack frame by frame until it crashes; iterative DFS keeps one call frame and grows a heap array instead, with no frame limit

One approach uses 10,000 call frames. The other uses one.

For how to do this systematically, the guide on converting recursion to iteration covers the general pattern.

BFS for Level-Order Work

For problems that process trees by level, BFS with a queue avoids deep call stacks by design. Memory grows with the widest level, not the depth of the tree. Binary Tree Level Order Traversal, Right Side View, and similar problems all fit this shape.

Trampoline for Recursive Style

Trampoline is a real word that means a real thing in computer science, not just something someone named to sound clever. It converts recursion into iteration by having functions return thunks (deferred function calls) instead of calling themselves directly. The outer loop bounces on those thunks until it gets a real value.

function trampoline(fn) { return function (...args) { let result = fn(...args); while (typeof result === 'function') { result = result(); } return result; }; } function sumThunk(n, acc = 0) { if (n <= 0) return acc; return () => sumThunk(n - 1, acc + n); // returns a thunk, not a direct recursive call } const sum = trampoline(sumThunk); sum(1_000_000); // no overflow

The trampoline wrapper loops until it receives a non-function value. Each "recursive" step is deferred to the next loop iteration, so the call stack never grows deeper than one frame at a time. The cost is function allocation per step, which makes trampolines slower than a clean iterative conversion for tight loops.

Trampolining is a valid interview answer when asked how to handle deep recursion while keeping recursive logic. Interviewers at companies with functional programming cultures sometimes ask about it directly. Most of the time, explicit-stack iteration is the cleaner answer, and "I'd convert this to iterative using an explicit stack" lands better than a seven-line trampoline implementation under time pressure.

The Python Comparison

Python caps recursion at 1,000 frames by default via sys.getrecursionlimit(). That number is configurable, documented, and raises RecursionError with a clean message when hit.

JavaScript has no exposed constant. The limit is engine-defined, varies between V8, SpiderMonkey, and JSC, and the error that surfaces is a generic RangeError. Python tells you the wall is there. JavaScript lets you sprint into it at full speed. The constraint is just as real as Python's; it is just less legible.

Python's limit is actually lower (1,000 versus V8's 10,000+), but Python is polite enough to advertise it. JavaScript has a higher limit and more aggressively pretends the limit does not exist. This is not a ringing endorsement of either language's design philosophy.

If you prep in both languages, the Python recursion limit guide covers how the same depth problem plays out there.

Quick Reference

EnvironmentApproximate frame limitNotes
V8 (Chrome, Node.js)10,000 to 15,000Drops with more locals per frame
SpiderMonkey (Firefox)25,000 to 30,000Varies by platform
JavaScriptCore (Safari)40,000 to 50,000Varies by platform
SituationRecommended fix
DFS on deep tree or graphIterative with explicit stack
Level-order tree traversalBFS with a queue
Recursive logic you want to keepTrampoline
Node.js script with known depth--stack-size flag (not for production)
Tail-recursive styleGood practice, no stack safety in V8

The Answer in an Interview

If recursion depth comes up, three things to cover:

Know the constraint exists: V8 allows around 10,000 to 15,000 frames, and it drops further based on frame size. Know how to fix it: convert to iterative with an explicit stack, or use BFS for level-order work. Know why TCO does not save you: V8 never implemented proper tail calls, and tail-recursive style still overflows the stack in Node.js.

The next step is practicing that explanation out loud under interview pressure. Saying "I'd convert this to iterative using an explicit stack" is one thing at your desk. Saying it clearly while an interviewer watches a timer is different. SpaceComplexity runs voice-based DSA mock interviews where you can rehearse follow-up questions like this before they count. Pattern recognition in silence is one skill. Explaining the constraint and proposing a fix in real time is a different one.

Further Reading