RangeError: Maximum Call Stack Size Exceeded — Recursion & Loop Fixes
What Does This Error Mean?
The call stack is JavaScript's list of functions currently running. Every function call adds a frame to the stack; when a function keeps calling itself (or functions call each other in a loop) without ever returning, the stack grows until the engine hits its limit — typically 10,000-15,000 frames in V8 — and throws RangeError: Maximum call stack size exceeded.
The Most Common Cause: Infinite Recursion
Recursive functions need a base case that stops the recursion. When the base case never triggers, the function calls itself forever:
// Bad — no base case that actually stops
function countdown(n) {
console.log(n)
return countdown(n - 1) // n goes negative forever
}
// Good — base case stops it
function countdown(n) {
if (n <= 0) return
console.log(n)
return countdown(n - 1)
}The fix is always: check your base case, and check the argument that feeds into it. A base case that can never be reached (like if (n === 0) when n skips 0 and goes -1) is the classic bug.
Indirect Recursion: Functions Calling Each Other
Two functions calling each other without an exit produces the same error with a different shape:
function a() { return b() }
function b() { return a() } // a -> b -> a -> b ... foreverThis often hides inside event handlers or callbacks: an event that triggers itself (a resize listener that changes the thing that triggers resize) is a real-world version of this. Look for any circular call chain in your stack trace — the trace shows the cycle repeating.
Runaway Loops That Look Like Recursion
Sometimes the error comes from a loop that never terminates because the state that should end it never updates:
// Bad — i never changes
let i = 0
while (i < 10) {
doSomething() // forgot i++
}
// Good
let i = 0
while (i < 10) {
doSomething()
i++
}The tell: the stack trace shows the same function at every level, and the line number is the same each time. A loop bug shows one repeating line; a recursion bug shows alternating lines.
Deep but Legitimate Recursion
Sometimes the recursion is correct but just too deep. Processing a deeply nested JSON structure (10,000+ levels) or traversing a huge tree can blow the stack even with a perfect base case. Fixes: (1) convert recursion to an explicit stack with a loop — the heap has far more room than the call stack; (2) use JSON.parse with a depth check for untrusted input; (3) for tree traversal, use a while loop with a stack array:
// Recursive (can overflow on deep trees)
function walk(node) {
if (!node) return
visit(node)
node.children.forEach(walk)
}
// Iterative — heap-based, no stack limit
function walk(node) {
const stack = [node]
while (stack.length) {
const current = stack.pop()
if (!current) continue
visit(current)
stack.push(...current.children)
}
}Circular Imports and the Same Error
In bundlers (Webpack, Vite) and Node ESM, circular imports can produce a confusing variant: module A imports from B, B imports from A, and at runtime one module is still initializing when the other tries to use it. The symptom is often this stack error at module scope, or a Cannot access before initialization. Fix: break the cycle — move shared code to a third module, or defer the usage until the module is initialized (inside a function rather than at module top level).
The 5-Minute Debug Process
1. Read the stack trace — the repeating frame is your culprit. 2. Check the repeating function for a base case or exit condition. 3. Verify the variable that should change between calls actually changes. 4. Search for event handlers that trigger their own event. 5. If the recursion is legitimate, convert to an iterative loop. If the error happens in a library, the bug is almost always in how you call it — a missing argument that should have ended the recursion.
Related reading: if you hit this while handling async data, the not a function guide covers the other classic runtime error, and unexpected token handles the syntax-level causes.