BugMojoBugMojoBugMojo
FeaturesPricingBlogHelpAbout
Add to ChromeLog inGet started
BugMojoBugMojo

Bug reports that actually help fix bugs — capture, replay, share.

A product of Softech Infra.

Product

  • Features
  • Pricing
  • Browser extension
  • Get started
  • Log in

Resources

  • Help & guides
  • Blog
  • Compare
  • Glossary

Company

  • About
  • Contact
  • Security
  • Privacy
  • Terms
  • Sitemap
© 2026 BugMojo. All rights reserved.
AllGuidesEngineeringPlaybooksCompareGlossaryAlternativesBy roleBug tracking by framework
  1. Home
  2. Blog
  3. Guides
  4. How to Fix "Maximum Call Stack Size Exceeded" in JavaScript
Guide

How to Fix "Maximum Call Stack Size Exceeded" in JavaScript

Maximum call stack size exceeded is JavaScript telling you a function called itself until it ran out of stack. Here is what the RangeError really means, the causes that trigger it, and how to fix and find the runaway recursion.

Hrishikesh BaidyaHrishikesh Baidya·Jul 16, 2026·10 min read
Guides
Isometric line-art of a JavaScript call stack of identical recursive frames stacking upward, the top frame cracked where the stack overflows, lime on a dark canvas
TL;DR
  • What it means: a RangeError thrown when the call stack — the engine's list of functions still waiting to return — grows past its fixed size limit.
  • The #1 cause: infinite or unterminated recursion. A function calls itself without ever reaching a base case, so frames pile up until the stack overflows.
  • Find it fast: read the stack trace. A long run of the same function name repeating is the recursion signature — that frame is the one calling itself.
  • Fix it: add or repair the base case; convert the recursion to a loop or an explicit stack/queue; use trampolining or batching for genuinely deep-but-finite data; debounce re-entrant event handlers.

Uncaught RangeError: Maximum call stack size exceeded is one of the most alarming errors a JavaScript developer hits, because it sounds like a memory limit or an engine bug and it usually appears with no useful line highlighted. It is almost never either of those. In the overwhelming majority of cases it means exactly one thing: a function is calling itself (directly or through a chain of other functions) and never stopping. This guide explains what the call stack is, why runaway recursion overflows it, the specific patterns that cause it, and how to both fix and find the offending function.

What the error actually means

Maximum call stack size exceeded is a RangeError thrown when the engine's call stack grows past its fixed capacity. Every function call pushes a frame; every return pops one. When calls keep pushing without returning — the hallmark of unterminated recursion — the stack fills and the engine aborts. The limit is a range boundary, hence RangeError.

The call stack is a data structure the JavaScript engine uses to track which functions are currently running. When you call a function, the engine pushes a frame onto the stack holding that call's local variables and the position to return to. When the function returns, its frame is popped off. At any moment the stack's height is how many nested calls are still waiting to finish. That stack has a fixed size — in V8, the engine behind Chrome and Node, it holds on the order of ten to fifteen thousand frames depending on how much each frame carries. Push one frame too many and the engine refuses, throwing RangeError: Maximum call stack size exceeded. Firefox phrases the same condition as InternalError: too much recursion; both mean the stack overflowed.

The word Range in RangeError is the tell. It is the same error family you get from new Array(-1) or a number outside a valid interval: you asked for something outside an allowed range. Here the range is the stack's capacity, and you exceeded it by nesting calls too deep.

The #1 cause: infinite recursion

A recursive function is one that calls itself. To be correct, it needs a base case: a condition under which it returns a value without recursing, so the chain of calls eventually unwinds. Remove that base case, or make it unreachable, and every call spawns another call that spawns another — frames stack up until the engine overflows. This is by far the most common source of the error.

1-missing-base-case.jsjavascript
// BROKEN: factorial with no base case — recurses forever
function factorial(n) {
  return n * factorial(n - 1);
}
factorial(5);
// RangeError: Maximum call stack size exceeded
// (n goes 5, 4, 3, 2, 1, 0, -1, -2 ... and never stops)

// FIXED: return without recursing once n reaches the boundary
function factorial(n) {
  if (n <= 1) return 1; // base case — the recursion terminates here
  return n * factorial(n - 1);
}
factorial(5); // 120

Just as common is a base case that exists but is never reached because the argument moves the wrong way, or never changes at all. The recursion has a stopping condition on paper, but the values never satisfy it.

2-unreachable-base-case.jsjavascript
// BROKEN: the base case exists but i never increases toward it
function printUpTo(i, max) {
  if (i > max) return;    // base case is here...
  console.log(i);
  printUpTo(i, max);      // ...but i is passed unchanged
}
printUpTo(0, 10);         // recurses forever with i stuck at 0

// FIXED: advance the argument so the base case is reachable
function printUpTo(i, max) {
  if (i > max) return;
  console.log(i);
  printUpTo(i + 1, max);  // i now climbs toward max
}
printUpTo(0, 10);

The other causes worth knowing

Not every overflow is a textbook recursive function with a missing if. Several patterns cause the same error without any obviously recursive code in sight.

Mutual recursion. Two (or more) functions call each other. Neither looks self-referential in isolation, but together they form a cycle with no exit.

3-mutual-recursion.jsjavascript
// BROKEN: isEven and isOdd call each other with no base case
function isEven(n) { return isOdd(n - 1); }
function isOdd(n)  { return isEven(n - 1); }
isEven(10); // RangeError: Maximum call stack size exceeded

// FIXED: give the cycle a shared exit condition
function isEven(n) {
  if (n === 0) return true;
  if (n === 1) return false;
  return isOdd(n - 1);
}
function isOdd(n) {
  if (n === 0) return false;
  if (n === 1) return true;
  return isEven(n - 1);
}
isEven(10); // true

Accidental self-reference. A getter, a toJSON, a toString, or a proxy trap that reads the very property it defines. The access triggers the accessor, which performs the same access, which triggers the accessor again.

4-self-referential-getter.jsjavascript
// BROKEN: the getter for `name` reads `this.name` — itself
const user = {
  get name() {
    return this.name; // re-invokes this same getter, forever
  },
};
user.name; // RangeError: Maximum call stack size exceeded

// FIXED: back the getter with a separate private field
const user = {
  _name: 'Ada',
  get name() {
    return this._name; // reads a different property — no recursion
  },
};
user.name; // 'Ada'

An event handler that re-triggers itself. A handler that, while responding to an event, does something that fires the same event synchronously — a change handler that sets the value it is watching, or a resize handler that resizes the element. Each dispatch runs the handler, which dispatches again.

A React setState-in-render loop. Calling a state setter directly in the body of a component (not inside an effect or an event handler) schedules a re-render, whose body calls the setter again, which schedules another render. React surfaces this as Maximum update depth exceeded, its own guard against the same runaway pattern.

5-react-setstate-in-render.jsxjavascript
// BROKEN: setCount runs during render → re-render → setCount → ...
function Counter() {
  const [count, setCount] = useState(0);
  setCount(count + 1); // called on every render
  return <span>{count}</span>;
  // Error: Maximum update depth exceeded
}

// FIXED: only update state in response to an event or effect
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount((c) => c + 1)}>{count}</button>
  );
}

Genuinely-too-deep-but-finite recursion. The rarest case: the recursion is correct and does terminate, but the data is so deep that it exhausts the stack before it finishes — walking a linked list of a million nodes, or a deeply nested JSON tree. Here nothing is wrong with the logic; the fix is to stop relying on the call stack as your data structure.

The fixes, matched to the cause

Add or repair the base case. For the classic infinite-recursion bugs, this is the whole fix: ensure there is a condition that returns without recursing, and that the recursive argument moves toward it on every call. If the base case exists but never fires, the bug is in how the argument changes, not in the condition.

Convert recursion to iteration. When recursion isn't essential, a loop removes stack growth entirely because it reuses one frame. For traversal problems, replace the implicit call stack with an explicit stack (an array with push/pop) for depth-first order, or a queue (push/shift) for breadth-first. The pending work now lives on the heap, which is effectively unbounded, so you can process arbitrarily deep structures.

6-recursion-to-explicit-stack.jsjavascript
// BROKEN: recursive tree walk overflows on very deep trees
function collect(node, out = []) {
  out.push(node.value);
  for (const child of node.children) collect(child, out);
  return out;
}

// FIXED: iterate with an explicit stack — no call-stack growth
function collect(root) {
  const out = [];
  const stack = [root];
  while (stack.length) {
    const node = stack.pop();
    out.push(node.value);
    // push children back-to-front to preserve left-to-right order
    for (let i = node.children.length - 1; i >= 0; i--) {
      stack.push(node.children[i]);
    }
  }
  return out;
}

Trampolining or batching for deep data. If you want to keep a recursive style but avoid deep stacks, a trampoline turns recursive calls into a loop: instead of calling itself, the function returns a thunk (a zero-argument function) describing the next step, and a driver loop keeps invoking thunks until it gets a real value. Because each step returns before the next begins, the stack never grows. For very large batches of independent work, you can also break the job across ticks with queueMicrotask or setTimeout, letting the stack unwind between chunks.

Debounce re-entrant handlers. When an event handler retriggers its own event, guard against re-entry: set a flag while the handler runs and ignore events that arrive during it, or debounce so a burst collapses into one call. For the React case, move the state update out of render and into an event handler or an effect with correct dependencies.

How to find the function that's recursing

The stack trace is the fastest way in, and this error leaves an unmistakable fingerprint. Where a normal trace shows a varied sequence of function names climbing from the throw site up to the entry point, a stack-overflow trace shows the same frame repeated — the same function, often the same line and column, over and over, frequently truncated by the engine because the real stack was thousands of frames tall. That repeated frame is your recursing function. If two names alternate, you are looking at mutual recursion.

overflow-stack-trace.txttext
Uncaught RangeError: Maximum call stack size exceeded
    at printUpTo (app.js:14:3)
    at printUpTo (app.js:16:3)   <-- same function
    at printUpTo (app.js:16:3)   <-- same line
    at printUpTo (app.js:16:3)   <-- repeating = recursion
    at printUpTo (app.js:16:3)
    ... (thousands more identical frames)

The engine is what builds that list. V8 exposes it through Error.stack and its stack-trace API, capturing frames as the error is thrown and formatting them into the text you see in the console. Once you have the repeating name, open that function and confirm two things: that a base case exists, and that the value it checks actually moves toward satisfying it on each call. If you can reproduce the crash, set a breakpoint inside the function or log its arguments on entry — you will watch the value that should terminate the recursion drift away from the boundary or sit frozen in place. For a deeper walkthrough of reading frames, see how to read a JavaScript stack trace.

Tip

If the overflow only happens for certain inputs or certain users, the trace alone rarely tells you which value drove the recursion off the rails. Capturing the session where it happened — the console output and the arguments that were flowing through — turns "it recurses sometimes" into "it recurses when this exact node points back at its own parent," which is the difference between guessing and fixing.

One habit worth building: treat any Maximum call stack size exceeded as a logic error, not a resource error. It is tempting to reach for engine flags that raise the stack limit (--stack-size in Node, for instance), but that only delays the overflow — a genuinely infinite recursion will still overflow a larger stack, just a fraction of a second later, and correct-but-deep recursion is better solved by moving off the call stack entirely. The size limit is doing its job by failing fast; the fix belongs in your termination condition or your data structure, not in the engine's configuration.

Key takeaway

The honest takeaway: Maximum call stack size exceeded is a RangeError that means recursion never stopped. Read the stack trace for the repeated frame to find the culprit, confirm the base case exists and is reachable, and fix it there — or, when the data is legitimately deep, move the work off the call stack with an explicit stack, a queue, or a trampoline. Raising the stack limit is not a fix.

⁓ ⁓ ⁓
See the exact call pattern that overflowed the stack

Install the free BugMojo extension and capture the session where the RangeError fired — the replay and console capture show the repeating call pattern and the arguments that triggered it, so you find the runaway recursion instead of guessing at it.

Install the free extension

Frequently asked questions

Frequently asked questions

Sources

  1. RangeError: too much recursion — MDN reference: the error thrown when recursion is too deep or has no terminating condition — MDN Web Docs (2026)
  2. RangeError — MDN reference: thrown when a value is outside the allowable range, including the call stack's size limit — MDN Web Docs (2026)
  3. Recursion — MDN glossary: a function that calls itself must have a base case to terminate — MDN Web Docs (2026)
  4. Stack Trace API — V8 documentation: how the engine captures and formats the call stack behind Error.stack — V8 (Google) (2026)
Share:
Hrishikesh Baidya
Hrishikesh Baidya· Chief Technology Officer

Hrishikesh Baidya is the CTO at Softech Infra. He is drawn to architecture that is invisible — systems that simply work — and leads the engineering behind BugMojo.

On this page

  • What the error actually means
  • The #1 cause: infinite recursion
  • The other causes worth knowing
  • The fixes, matched to the cause
  • How to find the function that's recursing

Get bug-tracking insights, weekly.

Engineering deep-dives, QA playbooks, and honest tool comparisons. No spam — unsubscribe in one click.