> ## Content Index
> Fetch the complete content index at: https://debugly.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# Maximum Call Stack Size Exceeded When It Is Not Recursion
- URL: https://debugly.dev/maximum-call-stack-size-exceeded/
- Published: 2025-10-28T14:00:00.000Z
- Updated: 2026-10-17T05:59:09.000Z
- Description: Sometimes it is infinite recursion. Often it is spreading a large array into a function call, and the fix is completely different.
- Author: Rohit Bhadani
- Tags: JavaScript, Error Autopsy, Node.js

## The quick version

```
RangeError: Maximum call stack size exceeded

```

Two very different causes:

1. **Infinite or very deep recursion.** The stack trace repeats the same few frames.
2. **Too many arguments in one call.** `Math.max(...bigArray)` or `arr.push(...bigArray)`. The stack trace has no repetition and points at a single line.

Look at the trace. If it repeats, find the base case. If it does not, look for a spread operator or `apply` with a large array.

Tested on Node 22.14 and Chrome 133.

## The non-recursive cause

This surprises people, so it is worth putting first.

```javascript
const values = new Array(200_000).fill(0).map((_, i) => i);
const max = Math.max(...values);      // RangeError

```

The spread does not iterate. It places every element as a separate argument on the stack. Two hundred thousand arguments exceeds the frame size limit, and you get a stack overflow from code containing no recursion at all.

Same problem in several disguises:

```javascript
arr.push(...otherBigArray);
target.concat.apply(target, bigArray);
String.fromCharCode(...bigByteArray);
Function.prototype.apply(null, bigArray);

```

The limit is engine and platform dependent, typically somewhere in the tens of thousands to low hundreds of thousands. Which means this works in testing with a thousand items and fails in production with a hundred thousand, and the error message points at recursion when there is none.

The fixes:

```javascript
// max without spread
const max = values.reduce((a, b) => (b > a ? b : a), -Infinity);

// push in chunks
for (let i = 0; i < src.length; i += 10_000) {
  target.push(...src.slice(i, i + 10_000));
}

// or just concat
const merged = target.concat(src);

// bytes to string
const decoder = new TextDecoder();
const str = decoder.decode(byteArray);

```

The general rule: **never spread an array whose length you do not control.** If it comes from a database, a file, or a user, chunk it or use a method that iterates.

## The recursive causes

### A missing or unreachable base case

```javascript
function walk(node) {
  process(node);
  for (const child of node.children) walk(child);   // fine unless there is a cycle
}

```

Correct for a tree, infinite for a graph with a cycle. If your data can have cycles, you need a visited set:

```javascript
function walk(node, seen = new Set()) {
  if (seen.has(node.id)) return;
  seen.add(node.id);
  process(node);
  for (const child of node.children) walk(child, seen);
}

```

Cycles show up in category trees where somebody made a category its own ancestor, in organisation charts, and in any user editable hierarchy. It is a data problem that presents as a code crash.

### Mutual recursion through a getter or proxy

```javascript
class User {
  get displayName() {
    return this.displayName || this.email;    // reads itself
  }
}

```

The getter reads the property it defines. Easy to write, easy to miss in review, and the trace repeats a single frame which makes it quick to spot once you look.

Same with a Proxy whose `get` handler accesses the property it is intercepting.

### JSON.stringify on a circular structure

Not a stack overflow in modern engines, which throw `TypeError: Converting circular structure to JSON` instead. Older environments and some custom serialisers still blow the stack.

If you need to serialise possibly circular data:

```javascript
JSON.stringify(obj, (function () {
  const seen = new WeakSet();
  return (k, v) => {
    if (typeof v === "object" && v !== null) {
      if (seen.has(v)) return "[Circular]";
      seen.add(v);
    }
    return v;
  };
})());

```

### Event handler loops

```javascript
input.addEventListener("change", () => {
  input.value = normalise(input.value);
  input.dispatchEvent(new Event("change"));   // triggers itself
});

```

Not classic recursion, and the effect is the same. Guard with a flag, or do not re-dispatch.

## Reading the trace

The trace tells you which case you have, and it is usually truncated.

```
RangeError: Maximum call stack size exceeded
    at walk (/app/tree.js:14:5)
    at walk (/app/tree.js:16:22)
    at walk (/app/tree.js:16:22)
    at walk (/app/tree.js:16:22)
    ...

```

Repetition means recursion. The two distinct line numbers tell you the entry and the recursive call.

For a deeper view:

```bash
node --stack-trace-limit=100 app.js

```

Useful when the recursion is mutual across several functions, where a short trace does not show the full cycle.

If the trace has no repetition and points at a single line with a spread or `apply`, you have the argument count problem.

## Raising the stack size, and why not to

```bash
node --stack-size=8000 app.js

```

This almost always makes things worse. It buys you a slightly deeper recursion before crashing, and if the real problem is unbounded recursion you still crash, just later and after more work. It can also cause a segfault rather than a clean `RangeError`, because you have told V8 it has more stack than the OS actually gave the thread.

The only case where I would use it is a known-bounded recursion that is genuinely deeper than the default, such as parsing a deeply nested but finite structure. Even then, converting to iteration is better.

## Converting recursion to iteration

For a genuinely deep structure, an explicit stack removes the limit:

```javascript
function walkIterative(root) {
  const stack = [root];
  const seen = new Set();

  while (stack.length) {
    const node = stack.pop();
    if (seen.has(node.id)) continue;
    seen.add(node.id);

    process(node);
    for (const child of node.children) stack.push(child);
  }
}

```

Heap allocated, so the limit is memory rather than stack depth. This handles millions of nodes.

Note that this is depth first with reversed sibling order. For breadth first use a queue and `shift`, though `shift` is O(n) on a large array so use a proper deque or an index pointer.

JavaScript has no tail call optimisation in practice. It is in the specification, and only Safari implemented it. So a tail recursive function still grows the stack in Node and Chrome, and rewriting recursion as a tail call does not help.

## Prevention

**Never spread unbounded arrays.** A lint rule can catch `...` inside a call where the argument is not a literal, though it is noisy. The habit matters more.

**Add a depth parameter to recursive functions** that walk external data:

```javascript
function walk(node, depth = 0) {
  if (depth > 100) throw new Error(`max depth exceeded at ${node.id}`);
  ...
}

```

That converts a stack overflow into an error message naming the node, which is [dramatically more useful](https://debugly.dev/error-messages-are-a-user-interface/) than a repeated stack frame.

**Use a visited set whenever the data could contain a cycle.** Assume it can if users can edit it.

**Test with realistic sizes.** A recursion depth or argument count problem is invisible with a small fixture and certain with a production sized one. Same category as [the N+1 query that only appears at scale](https://debugly.dev/bugs-ai-coding-agents-write/).

---

Published on [debugly.dev](https://debugly.dev/) by [Rohit Bhadani](https://debugly.dev/about/) | Founder of [Krova Cloud](https://krova.cloud/?ref=debugly.dev)