Maximum call stack errors come from excessive synchronous call nesting, usually through missing termination, cyclic traversal, recursive accessors, or reactive feedback. Engine limits vary and are not a portable design target.
Prove progress toward a base case, detect graph cycles, and use an explicit stack, queue, or bounded scheduled workflow when input depth is uncontrolled. Tests should enforce application limits before an engine reaches its own ceiling.
This error occurs when the call stack (the structure that stores function calls) exceeds its maximum size. This typically happens with infinite recursion or very deep function call chains.
// [wrong] Problem - Infinite recursion
function countdown(n) {
console.log(n);
countdown(n - 1); // No base case!
}
// [ok] Solution - Add base case
function countdown(n) {
if (n <= 0) return; // Base case
console.log(n);
countdown(n - 1);
}
// [ok] Solution - Use iteration instead
function countdown(n) {
for (let i = n; i > 0; i--) {
console.log(i);
}
}
// Factorial without base case
function factorial(n) {
return n * factorial(n - 1); // Infinite recursion!
}
factorial(5); // RangeError: Maximum call stack size exceeded
// Add base case
function factorial(n) {
if (n <= 1) return 1; // Base case
return n * factorial(n - 1);
}
factorial(5); // 120
// Or use iteration (better for performance)
function factorial(n) {
let result = 1;
for (let i = 2; i <= n; i++) {
result *= i;
}
return result;
}
function isEven(n) {
if (n === 0) return true;
return isOdd(n - 1);
}
function isOdd(n) {
if (n === 0) return false;
return isEven(n - 1);
}
isEven(-1); // Infinite loop! Never reaches base case
// Add validation for negative numbers
function isEven(n) {
n = Math.abs(n); // Handle negative numbers
if (n === 0) return true;
return isOdd(n - 1);
}
function isOdd(n) {
n = Math.abs(n); // Handle negative numbers
if (n === 0) return false;
return isEven(n - 1);
}
// Or use simple modulo (better)
function isEven(n) {
return n % 2 === 0;
}
function isOdd(n) {
return n % 2 !== 0;
}
const button = document.getElementById('myButton');
button.addEventListener('click', function() {
console.log('Button clicked');
button.click(); // Triggers itself infinitely!
});
// Solution 1: Remove the recursive call
const button = document.getElementById('myButton');
button.addEventListener('click', function() {
console.log('Button clicked');
// Don't trigger click again
});
// Solution 2: Use a flag to prevent recursion
let isProcessing = false;
button.addEventListener('click', function() {
if (isProcessing) return;
isProcessing = true;
console.log('Button clicked');
// Do your work
isProcessing = false;
});
// Solution 3: Remove listener before triggering
function handleClick() {
console.log('Button clicked');
button.removeEventListener('click', handleClick);
button.click(); // Now safe
button.addEventListener('click', handleClick);
}
button.addEventListener('click', handleClick);
// Sum array recursively
function sumArray(arr) {
if (arr.length === 0) return 0;
return arr[0] + sumArray(arr.slice(1));
}
const largeArray = new Array(100000).fill(1);
sumArray(largeArray); // RangeError: Maximum call stack size exceeded
// Solution 1: Use iteration
function sumArray(arr) {
let sum = 0;
for (let num of arr) {
sum += num;
}
return sum;
}
// Solution 2: Use reduce
function sumArray(arr) {
return arr.reduce((sum, num) => sum + num, 0);
}
// Solution 3: Tail recursion with trampoline (advanced)
function sumArray(arr, sum = 0) {
if (arr.length === 0) return sum;
return () => sumArray(arr.slice(1), sum + arr[0]);
}
function trampoline(fn) {
while (typeof fn === 'function') {
fn = fn();
}
return fn;
}
const largeArray = new Array(100000).fill(1);
trampoline(sumArray(largeArray)); // Works!
function Counter() {
const [count, setCount] = useState(0);
// [wrong] setState in render causes infinite loop
setCount(count + 1);
return <div>{count}</div>;
}
// Solution 1: Move setState to event handler
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
return (
<div>
<p>{count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
// Solution 2: Use useEffect for side effects
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
// Safe to call setState here
setCount(1);
}, []); // Empty dependency array = run once
return <div>{count}</div>;
}
Each active synchronous function call needs a stack frame containing return information and execution state. A call adds a frame and returning removes it. When calls continue without unwinding, the engine eventually refuses another frame and reports a `RangeError` in Chrome or Safari, or an `InternalError` in Firefox.
There is no portable maximum depth. It varies with the engine, version, environment, optimization, and frame contents. Code that succeeds at a measured depth on one machine can fail elsewhere, so increasing a runtime stack flag or publishing a browser-specific number is not a correctness strategy.
The stack trace often shows the same function or a short cycle of functions repeated. Inspect the earliest repeating pattern and the input that should have caused it to terminate. Minified stacks need release-matched source maps, and recursive data walkers need the path or node identifier recorded near the failure.
This error describes excessive synchronous nesting, not an ordinary long-running loop. An infinite loop blocks without growing the call stack, while repeated asynchronous callbacks usually unwind between tasks. Diagnose whether frames are accumulating before choosing a recursion, scheduling, or loop fix.
A recursive algorithm needs one or more base cases and a progress measure that moves every recursive call toward one of them. Checking `n === 0` is insufficient if negative, fractional, or non-finite values can arrive and the update never reaches zero. Validate the input domain before recursion and state why the measure must decrease.
Tree and graph traversal require different termination rules. A finite tree can still be too deep, while a graph can contain cycles. Track visited object identity or stable node IDs according to the data model. Mark a node before traversing its neighbors so a back edge cannot enter it again.
Mutual recursion can hide the cycle across modules or callbacks. Follow the entire call chain: validator to formatter to setter to observer and back to validator. A guard flag can prevent re-entry, but it must reset in `finally`; often the stronger repair is separating calculation from notification so one phase does not trigger itself.
Property accessors are functions. A getter that returns the same property and a setter that assigns the same property invoke themselves. Store the value in a distinct backing field, private field, or external state and test both read and write paths.
A listener can dispatch the same event synchronously, an observer can write the property it observes, and a render path can update state that immediately renders again. These are feedback cycles even when no function visibly calls itself. Draw the state transition and identify which edge should be one-way.
Do not solve a feedback cycle by adding an arbitrary recursion counter in production. Make updates idempotent, compare the new value with the committed value, separate render from effects, and schedule notifications only after a successful state transition. Framework state changes belong in lifecycle or event boundaries, not in pure rendering.
Proxy traps can recurse when a trap reads or writes through the proxy instead of the target. Reflection helpers can still trigger traps depending on the receiver and operation, so design proxy invariants carefully and test inherited accessors. Logging a proxied object may itself access properties and obscure the original cycle.
Serialization and cloning code must also define cycle behavior. JSON serialization rejects cycles, while a custom recursive serializer can overflow first. Either reject cyclic input with a path, preserve references with IDs, or use an API whose structured-clone behavior matches the requirement.
Convert deep traversal to an explicit stack or queue when input depth is uncontrolled. Store each node plus the state needed after its children. Depth-first traversal uses a stack, breadth-first traversal uses a queue, and both make memory limits, cancellation, visited tracking, and progress reporting visible.
Tail-position source code does not guarantee that the deployed JavaScript engine will optimize away frames. Do not rely on proper tail calls for portable browser or Node.js code. A trampoline can repeatedly invoke returned thunks without recursive frames, but an ordinary loop is usually simpler when the algorithm can be expressed directly.
Scheduling the next step with a task or microtask unwinds the current stack, but it changes timing and can still starve rendering if work is queued continuously. Chunk large work with a budget, yield through an appropriate scheduler, and support cancellation. For CPU-heavy independent work, a worker may protect the main thread.
Choose resource limits from the product contract: maximum graph nodes, maximum nesting, time budget, memory budget, and cancellation. Reject or truncate hostile input deliberately. Preventing a stack overflow is not enough if the replacement algorithm can consume unbounded heap or block input for seconds.
Test the base case, one recursive step, maximum supported depth, a value outside the valid domain, and a cyclic graph. For mutual recursion, cover every entry point. Assert the returned result and the resource policy, including a clear rejection when depth or node limits are exceeded.
Generate nested and cyclic structures programmatically so the fixture states its intended depth. Property-based tests can verify termination and agreement with an iterative reference implementation over many shapes. Keep generated sizes within the supported contract rather than probing an engine until it crashes.
During diagnosis, count calls or visited nodes and capture a short path sample, not every frame. Full recursive logging can consume memory, alter timing, and make the stack fail earlier. A debugger breakpoint conditioned on depth or node identity is usually more precise.
After repair, run the test in each supported runtime family because error types, source maps, and optimization differ. The algorithm should pass through its own documented bounds without relying on a particular engine ceiling, and should fail predictably before resource exhaustion beyond those bounds.
Try this next
0 of 2 completed
This error occurs when the call stack (which stores function calls) exceeds its limit. Common causes include infinite recursion, missing base cases in recursive functions, circular function calls, or very deep recursion with large datasets.
Add a base case to stop recursion, validate input to prevent infinite loops, use iteration instead of recursion for large datasets, or implement tail recursion optimization.
JavaScript does not define a portable call stack limit. The available depth varies by engine, version, environment, optimization, and stack-frame contents, so application code should enforce its own supported depth or use an iterative design.
Explore 500+ free tutorials across 20+ languages and frameworks.