RangeError: Maximum call stack size exceeded, explained and fixed
Every function call adds a frame to the call stack, and the stack has a limit. This error means calls nested so deeply that the limit was hit, which in practice almost always means recursion that never reaches its stopping condition: a function calling itself forever, two functions calling each other, or an event or setter triggering itself. Legitimate deep recursion exists, but it is rare next to the accidental kind.
How to confirm it
- Read the stack trace as a pattern, not a list. The same one or two function names repeat dozens or hundreds of times. Those are the functions in the cycle, and the error is inside their relationship, not at the “top” where it finally threw.
- Set a breakpoint or a counter in the repeating function and watch the arguments on each pass. In a correct recursion the arguments move toward the base case each time; in the broken kind they stay the same or move away from it.
The usual causes, most common first
- A missing or unreachable base case: the recursive function never hits the condition that stops it, or the condition exists but the recursive call does not actually progress toward it (same input passed again, an off-by-one that skips the exit value).
- A property setter or getter that assigns to itself:
set x(v) { this.x = v } invokes the setter again instead of storing to a backing field.
- An event that re-triggers itself: a change handler that sets the value it listens to, a resize handler that resizes, a watcher that writes the property it watches.
- Framework loops: updating state unconditionally during render or in an effect that depends on the state it sets, each update schedules the next.
- Mutual recursion with no exit: function A calls B, B calls A, and neither has a terminating branch for the actual inputs.
The fixes, in order
- Fix the progression: make each recursive call move measurably toward the base case, smaller input, decremented counter, a node closer to the leaf, and verify the base case is tested before the recursive call, not after.
- Store to a backing field in accessors: setters should write a differently named property (
this._x = v) or use a private field, never the accessor's own name.
- Break event self-triggering: compare before setting (skip the update when the value is unchanged), or set a re-entry flag, or move the write to a different event.
- When the recursion is genuinely deep and correct, convert it to an explicit loop with an array as the stack (tree walks and parsers are the usual candidates). Raising the stack limit exists in Node (
--stack-size) but is a last resort for verified-legitimate depth, not a treatment for a loop.
A stack overflow from recursion is a logic bug, not a capacity problem. If the same arguments appear on repeated frames, no stack size would ever be enough. Find the cycle first; only consider limits when the inputs demonstrably shrink toward an exit.
Common questions
What is the actual stack limit? It depends on the engine and the frame sizes involved, roughly on the order of ten thousand frames in browsers. The variation is why “it worked with small data” is common: depth grows with input size until it crosses the limit.
Why does it happen only for large inputs? Recursive depth often scales with the data, a tree, a nested comment thread, a long linked list. Small tests stay under the limit, production data exceeds it. The durable fix is an iterative version or a depth-safe structure.
Is this related to React's “Maximum update depth exceeded”? They are cousins: React's error is an update loop it detects itself, this one is the raw engine limit. Both mean something is invoking itself without a stopping condition.
Related errors