RangeError: Maximum call stack size exceeded - 10 Causes and Fixes

RangeError: Maximum call stack size exceeded means your code called functions faster than it returned from them, until the JavaScript engine ran out of room to track them. Nine times out of ten it is a recursion that never reaches its base case — the classic javascript recursion error — but the tenth time it is a getter calling itself, a React effect re-triggering its own dependency, or a perfectly fine function fed 200,000 arguments at once.

The stack is a fixed-size region the engine uses to remember where to return to. Every active call takes a frame. When frames exceed the limit, V8 throws this RangeError instead of corrupting memory.

This page gives you the one-line fix first, then ten distinct causes with runnable reproductions, then a checklist for the case where your stack trace is just the same two frames repeated 10,000 times and tells you nothing. Close-up of a laptop screen displaying code, set against a dark backdrop with blue lighting for a tech-focused ambiance.

ErrorRangeError: Maximum call stack size exceeded
Where it happensJavaScript — browser or Node.js, any framework. Thrown synchronously at the call site that overflowed. Firefox prints "InternalError: too much recursion"; Safari prints "RangeError: Maximum call stack size exceeded." with a trailing period.
What it meansToo many function calls are open at once — almost always a recursion with no reachable base case, or a single call spread over too many arguments.

The Fast Fix

Find the function that appears repeated in your stack trace and give it a base case that is actually reachable.

function walk(node) {
  if (!node) return;          // base case: the recursion can now end
  walk(node.next);
}

If the repeated frame is apply or a spread into Math.max / push, you are not recursing at all — you are passing too many arguments. Use a reduce or chunk the array instead:

const max = big.reduce((a, b) => (b > a ? b : a), -Infinity);

If the stack trace is empty or shows only framework frames, go to the diagnostic checklist.

What Is Actually Causing It

1. Recursion with no base case at all

Reproduce it

function countdown(n) {
  console.log(n);
  countdown(n - 1);
}

countdown(3); // 3, 2, 1, 0, -1, -2 ... RangeError

Why it happens — Every call pushes a new frame and nothing ever returns, so the stack grows until the engine refuses. n happily goes negative — numbers in JavaScript have no lower bound that stops you.

The fix

function countdown(n) {
  if (n <= 0) return;   // added: the recursion now terminates
  console.log(n);
  countdown(n - 1);
}

countdown(3);

Confirm it worked — Call it with the largest input you expect and confirm it returns. Add a temporary console.log right before the return in the base case — if it never prints, your base case is unreachable.


2. Base case exists but the argument never moves toward it

Reproduce it

function sum(arr, i = 0) {
  if (i >= arr.length) return 0;
  return arr[i] + sum(arr, i); // i never advances
}

sum([1, 2, 3]); // RangeError

Why it happens — The guard is correct, but the recursive call passes the same i forever, so the condition i >= arr.length is never satisfied. This is the most common variant in real code because the base case looks right in review.

The fix

function sum(arr, i = 0) {
  if (i >= arr.length) return 0;
  return arr[i] + sum(arr, i + 1); // i + 1: progress toward the base case
}

sum([1, 2, 3]); // 6

Confirm it worked — Log the changing argument on entry: console.log('sum', i). The values must strictly approach the base case. If you see the same value twice in a row, that is your bug.


3. Two functions call each other indirectly

Reproduce it

function isEven(n) { return isOdd(n - 1); }
function isOdd(n)  { return isEven(n - 1); }

isEven(10); // RangeError — neither one stops

Why it happens — Mutual recursion still consumes stack frames, but neither function has a terminating branch, so each delegates to the other forever. Your stack trace alternates two frames, which makes it easy to miss that the cycle is the bug.

The fix

function isEven(n) { return n === 0 ? true  : isOdd(n - 1); }
function isOdd(n)  { return n === 0 ? false : isEven(n - 1); }

isEven(10); // true

Confirm it worked — Read the top 10 lines of the stack trace. If two or more distinct function names repeat in a cycle, each participant needs its own terminating branch — confirm by calling with the boundary value (0 here).


4. A property getter or setter reads itself

Reproduce it

const user = {
  get name() {
    return this.name; // reads the getter it is defined by
  }
};

user.name; // RangeError

Why it happens — this.name triggers the same accessor, which triggers it again. There is no explicit recursive call in the source, so the trace shows only get name repeated and the cause is invisible unless you know this pattern.

The fix

const user = {
  _name: 'ada',
  get name() {
    return this._name; // read the backing field, not the accessor
  }
};

user.name; // 'ada'

Confirm it worked — Check that the identifier inside the accessor differs from the accessor's own name. In a debugger, break inside the getter and confirm the frame below it is the caller, not the getter again.


5. Spreading or applying a huge array as arguments

Reproduce it

const big = new Array(200000).fill(1);
Math.max(...big); // RangeError: Maximum call stack size exceeded

Why it happens — Spread and Function.prototype.apply place every element on the stack as a separate argument for a single call. There is no recursion at all — one call with enough arguments overflows by itself. The limit is engine-dependent, typically in the low hundreds of thousands.

The fix

const big = new Array(200000).fill(1);
const max = big.reduce((a, b) => (b > a ? b : a), -Infinity); // one frame, any length

Confirm it worked — Shrink the array to 100 elements. If the error disappears with no logic change, argument count was the cause, not recursion. For arr.push(...items), loop in chunks of a few thousand instead.


6. Circular reference walked by a recursive traversal

Reproduce it

const a = { name: 'a' };
const b = { name: 'b', peer: a };
a.peer = b;

function collect(node, out = []) {
  out.push(node.name);
  if (node.peer) collect(node.peer, out); // a -> b -> a -> b ...
  return out;
}

collect(a); // RangeError

Why it happens — The recursion is correct for a tree but the data is a graph. Each node leads back to one already visited, so the depth is unbounded even though the object count is two.

The fix

function collect(node, out = [], seen = new WeakSet()) {
  if (seen.has(node)) return out; // added: stop on a node already visited
  seen.add(node);
  out.push(node.name);
  if (node.peer) collect(node.peer, out, seen);
  return out;
}

collect(a); // ['a', 'b']

Confirm it worked — Run JSON.stringify(root) on the same data. If it throws TypeError: Converting circular structure to JSON, your structure has a cycle and the recursion needs a visited set.


More causes (4 remaining)

7. Recursion is correct but the input is too deep

Reproduce it

// Build a 100k-long linked list, then walk it recursively.
let head = null;
for (let i = 0; i < 100000; i++) head = { value: i, next: head };

function length(node) {
  return node === null ? 0 : 1 + length(node.next);
}

length(head); // RangeError on large inputs

Why it happens — The base case is reachable and the argument advances — the algorithm is right. The depth simply exceeds the stack. JavaScript engines do not reliably eliminate tail calls, so depth-proportional recursion is depth-proportional stack usage.

The fix

function length(node) {
  let n = 0;
  while (node !== null) { n++; node = node.next; } // iteration: constant stack
  return n;
}

length(head); // 100000

Confirm it worked — Halve the input size. If it succeeds at 50k and fails at 100k, depth is the problem, not logic — convert to a loop or an explicit stack array.


8. A method override calls itself instead of the original

Reproduce it

const originalLog = console.log;
console.log = function (...args) {
  console.log('[app]', ...args); // calls the replacement, not the original
};

console.log('hi'); // RangeError

Why it happens — The wrapper references the property it just overwrote, so every call re-enters the wrapper. The same shape appears with super-less class overrides and monkey-patched fetch or addEventListener.

The fix

const originalLog = console.log;
console.log = function (...args) {
  originalLog.call(console, '[app]', ...args); // call the captured original
};

console.log('hi'); // [app] hi

Confirm it worked — Confirm the wrapper body calls the saved reference, never the patched property name. Trigger it once and check the output appears exactly once.


9. React: setState inside render or an effect that depends on its own output

Reproduce it

function Counter() {
  const [items, setItems] = useState([]);

  useEffect(() => {
    setItems([...items, 1]); // new array identity -> effect runs again
  }, [items]);

  return <div>{items.length}</div>;
}

Why it happens — The effect writes state that is in its own dependency array, and the new array has a different identity every time, so React re-runs the effect immediately. The synchronous render/update cycle nests deep enough to overflow the stack.

The fix

function Counter() {
  const [items, setItems] = useState([]);

  useEffect(() => {
    setItems(prev => [...prev, 1]); // functional update, no items dependency
  }, []); // runs once

  return <div>{items.length}</div>;
}

Confirm it worked — Add console.count('effect') as the first line of the effect. It must settle at a small number. With React StrictMode in development, expect exactly two runs, not a climbing count.


10. An event handler dispatches the event it listens for

Reproduce it

const input = document.querySelector('input');

input.addEventListener('change', () => {
  input.dispatchEvent(new Event('change')); // re-enters this handler synchronously
});

Why it happens — dispatchEvent runs listeners synchronously, so the handler calls itself through the event system. Same mechanism with a focus handler that calls .focus(), or a resize handler that changes layout size.

The fix

const input = document.querySelector('input');
let dispatching = false;

input.addEventListener('change', () => {
  if (dispatching) return; // reentrancy guard
  dispatching = true;
  try {
    input.dispatchEvent(new Event('change'));
  } finally {
    dispatching = false;
  }
});

Confirm it worked — Look for the DOM API name (dispatchEvent, focus, click) repeated in the stack trace. After the guard, interact once and confirm the handler body logs once.

A child carefully plays Jenga, focusing on precision and balance, outdoors on a patterned mat. ## None of Those? Narrow It Down
  1. Read the stack trace from the top and find the shortest repeating cycle of frame names. One name repeated means direct recursion; two or more alternating means mutual recursion; get X or set X repeated means an accessor reading itself.
  2. If the repeated frame is apply, Function.prototype.apply, or a spread into a built-in like Math.max or Array.prototype.push, stop looking for recursion — you are passing too many arguments in a single call. Shrink the array to confirm.
  3. Halve the input size and re-run. If it now succeeds, your logic is correct and the depth is the problem: convert the recursion to a loop or an explicit stack. If it fails identically on a two-element input, the base case is unreachable.
  4. Log the recursive function's changing argument on entry. Identical consecutive values prove the argument is not advancing; values cycling through a small set prove your data has a cycle.
  5. Run JSON.stringify(input) on the data you are traversing. A TypeError: Converting circular structure to JSON confirms a cycle and tells you a WeakSet of visited nodes is the fix.
  6. If the trace shows only framework frames (React reconciler, Vue scheduler, Express layers), look for state written during render or inside an effect that depends on its own output. Add console.count at the entry of each suspect effect or handler.
  7. If the trace is empty or truncated, re-run in Node with node --stack-trace-limit=200 script.js to see more frames, or set Error.stackTraceLimit = 200 at the top of the file in a browser.
  8. If nothing above matches, binary-search the code path: comment out half the work inside the suspect function and re-run. The half that still throws contains the call that re-enters.

Why This Error Exists At All

Every function call needs somewhere to store its return address, its arguments, and its local variables. JavaScript engines keep that in a call stack: a contiguous, fixed-size block of memory allocated when the thread starts. Fixed size is the whole point — pushing and popping a frame is a pointer bump, which is why calling a function is cheap. A growable stack would make every call pay for a bounds check and a possible reallocation.

The consequence is a hard ceiling. When the next frame would run past the end of that block, the engine has two options: write past it and corrupt whatever sits next in memory, or refuse. V8 refuses, and the refusal surfaces as a RangeError — the same error class used for an out-of-range array length or an invalid toFixed digit count, because the engine really is reporting "this value is outside the range I can accept." The size is an implementation detail, not a spec guarantee, which is why your recursion survives 10,000 frames in Chrome and dies at a different depth in Safari or inside a Web Worker.

This also explains why the error has nothing specifically to do with recursion. Recursion is just the easiest way to open thousands of calls without returning from any of them — but spreading 200,000 arguments into one call consumes the same space in one shot, and so does a deeply nested synchronous event dispatch. Once you see the error as "too many open frames" rather than "bad recursion," the whole family becomes recognisable: deep recursion, mutual recursion, self-referencing accessors, reentrant event handlers, and argument-count overflow are five faces of one resource limit. And it explains the escape hatch: anything that lets a frame return before the next one opens — a while loop, an explicit array used as a worklist, or deferring to a microtask with queueMicrotask — makes the depth irrelevant, because the stack never holds more than one frame at a time. Close-up of a computer monitor displaying cyber security data and code, indicative of system hacking or programming.

Stop It From Coming Back

  • Write the base case first, before the recursive call, and make the terminating condition the first statement in the function body. A guard clause at the top is hard to render unreachable later.
  • Enable ESLint's no-unsafe-negation and no-self-assign, and add eslint-plugin-react-hooks with react-hooks/exhaustive-deps set to error — it catches the effect-depends-on-its-own-state pattern before it ships.
  • For any traversal of user-supplied or API-supplied object graphs, thread a WeakSet of visited nodes through the recursion as a standard parameter. Treat tree-shaped assumptions about external data as unverified.
  • Add a depth parameter with an explicit cap (if (depth > 1000) throw new Error('max depth')) to recursive parsers and serializers. A domain error at depth 1000 is debuggable; a RangeError at depth 11,000 is not.
  • Unit-test recursive functions with a deliberately deep input — a 50,000-element linked list or a 10,000-level nested object — so depth limits fail in CI rather than on a large production payload.
  • Replace fn(...array) and fn.apply(null, array) with reduce, a loop, or fixed-size chunking whenever the array length is not statically bounded. Code-review any spread whose source is a database or API response.

Related Guides

Errors You Will Probably Hit Next

  • TypeError: Converting circular structure to JSON
  • InternalError: too much recursion (Firefox's wording for the same overflow)
  • Error: Maximum update depth exceeded (React's guard before the stack itself overflows)

Read this error as "too many open frames," not "bad recursion" — that reframing covers the argument-spread and reentrant-handler cases that look nothing like recursion in the source. Find the shortest repeating cycle in the stack trace, then ask two questions: does a base case exist, and does every call move measurably closer to it. When the answer to both is yes, the algorithm is fine and the depth is the problem, so switch to iteration or an explicit worklist array.

댓글

이 블로그의 인기 게시물

TypeError: Cannot read properties of undefined (reading 'map') - 11 Causes and Fixes

psql: FATAL: password authentication failed for user - 10 Causes and Fixes

Error: ERR_MODULE_NOT_FOUND: Cannot find package - 10 Causes and Fixes