RangeError: Maximum Call Stack Size Exceeded When Processing JSON — Diagnosis and Fix

Quick answer

💡Maximum call stack size exceeded when processing JSON is caused by custom recursive traversal functions running out of V8 stack frames — not by JSON.parse itself, which is written in C++ and handles deep nesting safely. Replace recursive tree-walking code with an explicit work queue (array-as-stack) that uses heap memory instead of call frames, or add a depth limit to reject input above a safe threshold.

Error symptoms

  • RangeError: Maximum call stack size exceeded in Node.js or browser console
  • InternalError: too much recursion in Firefox
  • Custom JSON formatter, validator, or sanitizer crashes on deeply nested input
  • JSON schema validator throws a stack overflow when validating payloads with many nested levels
  • Node.js process exits with exit code 1 when processing a large nested object from an external API
  • Browser tab freezes or becomes unresponsive when a recursive function walks a deeply nested JSON tree

Common causes

  • Custom recursive traversal functions that walk JSON trees without a maximum depth guard
  • Schema validators like Ajv processing deeply nested $ref chains that recurse into themselves
  • A JSON formatter that recursively pretty-prints each node without limiting the call depth
  • Deep-clone or sanitize utilities that use recursion to copy nested objects with no termination guard
  • A comment thread or folder tree structure where nesting levels routinely exceed a thousand
  • User-supplied JSON in a tool or API that contains pathological nesting generated to trigger a denial-of-service

When it happens

  • When processing user-submitted JSON in a developer tool or API endpoint that accepts arbitrary input
  • When traversing deeply nested AST representations, folder trees, or recursive data structures
  • When a schema validator encounters a JSON schema with many levels of nested object definitions
  • When a logging or serialization utility tries to walk an in-memory object graph that has unusual depth
  • When comment threading or tree-based data structures grow organically beyond what the original recursion budget assumed

Examples and fixes

A recursive function that collects all text nodes from a comment thread fails when the nesting depth exceeds the V8 call stack limit.

Recursive tree traversal crashing on deep comment threads

❌ Wrong

function collectCommentText(commentNode) {
  const results = [commentNode.text];
  if (commentNode.replies && commentNode.replies.length > 0) {
    for (const reply of commentNode.replies) {
      // Recursive call for each reply — no depth guard
      results.push(...collectCommentText(reply));
    }
  }
  return results;
}

// Crashes when threadRoot has 12,000+ levels of nesting
const allText = collectCommentText(threadRoot);

✅ Fixed

function collectCommentText(threadRoot) {
  const results = [];
  const workQueue = [threadRoot];

  while (workQueue.length > 0) {
    const commentNode = workQueue.pop();
    results.push(commentNode.text);

    if (commentNode.replies && commentNode.replies.length > 0) {
      // Push children onto heap-based queue instead of calling recursively
      for (const reply of commentNode.replies) {
        workQueue.push(reply);
      }
    }
  }

  return results;
}

const allText = collectCommentText(threadRoot);

Every recursive function call in JavaScript consumes one stack frame. The V8 engine allows approximately 10,000 to 15,000 frames before throwing RangeError. The iterative version replaces recursive calls with a plain array that acts as an explicit stack. Array push and pop use heap memory, which is limited only by available RAM rather than the call stack budget. The logic is identical — depth-first traversal using a LIFO queue — but it scales to arbitrarily deep trees without any risk of stack overflow.

A JSON sanitizer that strips disallowed keys should reject input that exceeds a safe nesting depth before attempting to traverse it, to prevent denial-of-service from adversarial payloads.

Adding a depth limit to reject pathologically nested JSON

❌ Wrong

function sanitizePayload(node, allowedKeys) {
  if (typeof node !== 'object' || node === null) return node;
  const cleaned = {};
  for (const [key, value] of Object.entries(node)) {
    if (allowedKeys.includes(key)) {
      // Recurse without depth tracking — vulnerable to deep nesting
      cleaned[key] = sanitizePayload(value, allowedKeys);
    }
  }
  return cleaned;
}

const result = sanitizePayload(userSuppliedJson, ['name', 'age', 'address']);

✅ Fixed

function measureDepth(node, currentDepth = 0, maxDepth = 64) {
  if (currentDepth > maxDepth) return currentDepth;
  if (typeof node !== 'object' || node === null) return currentDepth;
  return Math.max(
    ...Object.values(node).map(v => measureDepth(v, currentDepth + 1, maxDepth))
  );
}

function sanitizePayload(node, allowedKeys, depth = 0, maxDepth = 64) {
  if (depth > maxDepth) throw new Error('Payload nesting exceeds maximum allowed depth');
  if (typeof node !== 'object' || node === null) return node;
  const cleaned = {};
  for (const [key, value] of Object.entries(node)) {
    if (allowedKeys.includes(key)) {
      cleaned[key] = sanitizePayload(value, allowedKeys, depth + 1, maxDepth);
    }
  }
  return cleaned;
}

const result = sanitizePayload(userSuppliedJson, ['name', 'age', 'address']);

The depth parameter passed through each recursive call tracks how deep into the tree the current execution is. When depth exceeds maxDepth, the function throws a controlled error with a clear message rather than crashing the process with a stack overflow. A limit of 64 levels is generous for realistic payloads — deeply nested real data is rare — and makes the function immune to adversarial inputs that deliberately maximize nesting to exhaust the call stack. The thrown error can be caught by the HTTP handler and returned as a 400 Bad Request response.

Call stack limits from recursive JSON traversal

The JavaScript engine maintains a call stack to track active function invocations. Each function call pushes a stack frame containing the function's local variables, parameters, and return address. The V8 engine, which powers Node.js and Chrome, allows approximately 10,000 to 15,000 frames before throwing RangeError: Maximum call stack size exceeded. The exact limit varies with the size of each frame — functions with more local variables consume more stack space per frame, lowering the effective depth limit.

A common misconception is that JSON.parse or JSON.stringify is the source of the overflow. Both are implemented as native C++ code inside V8 and do not use the JavaScript call stack for their internal recursion. JSON.parse can successfully parse JSON with hundreds of thousands of nesting levels without throwing a stack error. The RangeError comes from JavaScript code — specifically from recursive functions that developers write to traverse, validate, transform, or sanitize JSON-derived objects after parsing.

The trigger is almost always a recursive function that was written and tested with realistically shallow data. The code works fine for objects with five or ten levels of nesting, which covers the vast majority of API payloads. But when someone passes it a comment thread with twelve thousand reply levels, a folder tree that was auto-generated by a build tool, or a deliberately adversarial payload sent to test security boundaries, the recursion depth exceeds the stack budget and the process crashes.

Schema validators present a less obvious version of the same problem. A validator like Ajv processes JSON schemas that can use $ref to reference other schemas, which the validator resolves recursively. A schema with deeply nested object definitions — even if the actual JSON data is shallow — can drive the validator into deep recursion during the schema compilation or validation phase. This is a separate code path from the user-supplied data traversal but produces the same RangeError.

Profiling stack depth with Node.js --stack-trace-limit

When RangeError: Maximum call stack size exceeded appears, the default stack trace in Node.js shows only ten frames, which is often not enough to identify the recursive function that caused the overflow. Increase the trace length by launching Node.js with the flag --stack-trace-limit=50 or by setting Error.stackTraceLimit = 50 at the top of your application. This does not change when the error occurs, but it makes the stack trace long enough to show the repeating function name that is the source of the recursion.

If the repeated function name appears in your own code, you have found the recursive traversal that needs a depth guard or an iterative replacement. If the repeated function name is inside a library — ajv, fast-deep-equal, lodash, or similar — check whether the library exposes a depth limit option, and consider preprocessing the input to ensure it does not exceed a safe depth before passing it to the library.

Measuring the depth of a parsed JSON object before processing is a useful early check. A simple iterative depth measurement function traverses the object using a work queue and tracks the maximum depth encountered. If the measured depth exceeds a threshold — a reasonable default is 64 for API payloads — reject the input immediately with a clear error rather than attempting to process it. This check should happen as early as possible in the request handling pipeline, before any expensive traversal code runs.

Node.js's --max-old-space-size flag does not help with stack overflow issues, which are a separate resource from heap memory. If increasing heap size resolves an apparent stack overflow, the actual cause is likely a different memory issue. True stack overflows respond only to reducing call depth, adding depth guards, or switching to iterative algorithms.

Replacing recursion with an explicit work queue

The fundamental fix for any recursive JSON traversal that might encounter deep nesting is to replace the recursion with an explicit work queue implemented as an array. Instead of calling the function with a child node, push the child node onto the array and process it in the next loop iteration. The array acts as a stack — push to add work, pop to retrieve it — and lives in heap memory rather than the call stack.

Converting from recursive to iterative form follows a consistent pattern. For depth-first traversal, use an array as a stack with push and pop. For breadth-first traversal, use an array as a queue with push and shift, though shift is O(n) and a proper queue implementation with a head pointer is more efficient for large trees. For transformations that need to construct a result from nested children — like a formatter that builds an indented string — maintain a parallel result stack that accumulates results from child nodes as they are processed.

The iterative form requires more thought than the recursive form, particularly for transformations where the result of processing a node depends on the results of processing its children. In recursive code, this is natural — the recursive call returns before the parent uses the result. In iterative code, you need to track which nodes have been processed and ensure their results are available when their parent node is completed. A common approach is a two-pass strategy: first collect all nodes in traversal order, then process them in reverse order so that child results are available when their parents are processed.

For cases where you want to keep the recursive style but need to handle deep nesting, trampolining is a functional programming technique that converts deep recursion into a loop externally. A trampoline wrapper calls a function, checks whether the return value is another function (meaning the computation is not yet complete), and if so calls that function — iterating until a final value is returned. This avoids deep call stacks while preserving recursive logic internally, though it requires restructuring the recursive function to return continuations.

Streaming parsers versus recursive parsers

When the goal is parsing extremely large or deeply nested JSON rather than traversing an already-parsed object, streaming parsers provide a fundamentally different approach that avoids both memory and stack issues. A streaming parser processes the JSON token by token — opening brace, key string, colon, value, closing brace — emitting events as each token is encountered rather than building the complete object in memory first.

The clarinet and jsonparse npm packages are streaming JSON parsers for Node.js. They work similarly to SAX parsers for XML: you register event handlers for tokens like openobject, key, value, and closeobject, and your code responds to each token as it arrives. Because there is no recursion and no complete in-memory object, streaming parsers have no practical limit on nesting depth and constant memory usage regardless of object size.

Streaming is most valuable when the JSON payload is either extremely large (hundreds of megabytes), extremely deeply nested, or both. For a payload that is large but shallow, streaming saves memory. For a payload that is small but deep, streaming saves stack space. For payloads that are both, streaming saves both. The trade-off is that streaming code is more complex to write — you must maintain state manually rather than relying on the natural scope of recursive function calls.

JSON.parse with a reviver function is sometimes mistaken for streaming, but it is not — it parses the complete JSON string into a full object first, then walks that object calling the reviver for each value in a bottom-up order. The reviver receives already-parsed values, not raw tokens, so the full object is in memory during reviver execution. For deep nesting issues, a reviver does not help because JSON.parse itself does not use the JavaScript stack for its own recursion.

Deeply nested comment threads hitting stack limits

Comment threading systems are one of the most common real-world sources of this error because the nesting structure grows organically without a visible upper bound. A user creates a comment, someone replies, another person replies to that reply, and so on. Most threads stay shallow, but a few threads on popular content accumulate hundreds of nested levels. The database stores each comment with a parent_id reference, and the API query reconstructs the tree by recursively joining parent to child.

The server-side tree building is typically the first place the stack overflow appears. A function that takes a flat list of comments with parent IDs and recursively assembles them into a nested tree structure works fine for threads with fifty replies but crashes for threads with fifteen thousand. The fix is to use an iterative approach: sort the flat list by depth, create a map of id to node object, and then iterate through the sorted list assigning each node to its parent's children array. This O(n) iterative approach has no stack depth constraint.

The client side presents the same problem when rendering the nested tree as DOM elements. A React component that renders itself recursively for child comments, or a Vue component that recursively mounts child comment components, will hit the JavaScript stack limit for sufficiently deep threads. The common fix is to flatten the visual representation — show only a fixed number of nesting levels visually and collapse deeper threads behind a 'load more' interaction, which also avoids the UX problem of infinitely wide indented threads.

Another pattern that triggers this in unexpected ways is server-side rendering of deeply nested UI component trees. When a framework like Next.js renders components to HTML on the server, it typically calls render functions recursively. A component tree that is thirty levels deep is unusual but possible for complex layouts, and can cause stack overflows during SSR that do not appear during client-side rendering where the same components are mounted iteratively by the browser's event loop.

Depth budgets for accepted JSON payloads

The most reliable way to prevent stack overflow errors from user-supplied JSON is to enforce a depth budget at the API boundary before any recursive processing occurs. A depth budget is a maximum nesting level that the API will accept — typically between 20 and 100 for general-purpose APIs, and lower for specialized endpoints where the expected payload shape is well-defined.

Implement the depth check as middleware that runs before any request handler. Parse the raw request body, measure its depth iteratively, and return a 400 Bad Request response with a descriptive error message if the depth exceeds the budget. The client receives a clear error rather than a 500 caused by a stack overflow, and the server is protected from denial-of-service via deep nesting. Measuring depth iteratively is important — using a recursive depth measurement function to guard against deep input that crashes recursive processing is self-defeating.

Document the depth budget in your API reference alongside size limits and rate limits. Developers consuming your API need to know that payloads must stay within a nesting depth to be accepted. If your use case genuinely requires deep nesting — for example, an API that stores arbitrary file system trees — document the limit and provide guidance on how to flatten or paginate deep structures.

For internal tools and developer utilities that process user-supplied JSON interactively, show a clear and friendly error when the input exceeds the depth budget rather than crashing silently. A message like 'This JSON is nested 12,847 levels deep — the maximum supported depth is 100' is vastly more helpful than a stack trace. Adding a nesting depth indicator to the validator output gives developers visibility into the structure of their payload before they hit the limit.

Quick fix checklist

  • Run Node.js with --stack-trace-limit=50 to see which recursive function is repeating in the stack trace
  • Add a depth parameter to recursive traversal functions and throw a controlled error when it exceeds a safe limit
  • Replace recursive tree traversal with an iterative loop using an array as an explicit work queue
  • Measure JSON payload depth iteratively before passing to any recursive processor or schema validator
  • Reject payloads above a depth budget (64 is generous) at the API boundary with a 400 response
  • Check schema validators like Ajv for depth limits on $ref chains and deeply nested schema definitions
  • Use clarinet or jsonparse for streaming token-by-token processing of arbitrarily deep JSON
  • Add a depth indicator to developer tools that process user-supplied JSON to make deep nesting visible

Related guides

Frequently asked questions

Does JSON.parse throw maximum call stack exceeded for deep JSON?

No. JSON.parse is implemented as native C++ code inside V8 and does not use the JavaScript call stack for its internal recursion. It can parse JSON with hundreds of thousands of nesting levels without throwing RangeError. The stack overflow comes from JavaScript code that processes the already-parsed object — custom traversal functions, schema validators, formatters, or cloners written in JavaScript.

What is the maximum nesting depth before hitting the V8 stack limit?

The limit depends on how much stack space each recursive call uses. A simple function with few local variables might allow 10,000 to 15,000 levels of nesting before crashing. A more complex function with more local variables uses more stack space per frame and hits the limit at a shallower depth. Plan for a safe limit of 64 to 100 levels in user-facing APIs and test your recursive functions with pathologically deep inputs.

How do I convert a recursive JSON walker to iterative?

Replace the recursive call with a push onto an array that acts as a work queue. At the top of the function, pop from the queue and process the node. Continue until the queue is empty. For depth-first traversal, use array push and pop. For breadth-first, use push and shift. The logic is the same as recursion, but all pending work lives in heap memory rather than the call stack.

Can I increase the JavaScript stack size in Node.js?

Yes, but it is a temporary workaround rather than a fix. Pass --stack-size=65536 to the node command to set the stack size in kilobytes. This raises the frame limit but does not eliminate it, and the optimal fix is still to convert the recursive code to iterative. Increasing the stack size also increases the per-thread memory footprint, which matters for servers handling many concurrent requests.

Why does my schema validator throw stack overflow on valid JSON?

Schema validators like Ajv process the JSON schema itself recursively, not just the data. A schema with deeply nested object definitions, multiple levels of allOf or oneOf, or $ref chains that resolve through many intermediate schemas can drive the validator into deep recursion during compilation or validation. Check the validator's documentation for depth limits, or simplify the schema by flattening definitions.

Is using a streaming JSON parser always better for deep JSON?

Streaming parsers eliminate stack and memory concerns for deeply nested or large JSON, but they require more complex application code. You respond to token events rather than traversing a complete object, which means maintaining state manually. For most API payloads that have bounded depth and reasonable size, a depth guard on recursive code is simpler. Use streaming when depth or size are genuinely unbounded.

How do I measure the depth of a JSON object iteratively?

Use a work queue where each entry is a tuple of the current node and its depth. Initialize the queue with [rootNode, 0]. On each iteration, pop a tuple, update the maximum depth seen, and push all child nodes with depth incremented by one. When the queue is empty, return the maximum depth. This O(n) traversal has no stack depth constraint and safely measures objects of any depth.

What depth limit should I set for a public API?

A limit of 20 to 32 levels is appropriate for most REST APIs where the payload schema is well-defined. Developer tools or APIs that accept arbitrary JSON can be more generous — 64 to 100 levels — while still being immune to pathological inputs. Document the limit in your API reference and return a 400 response with a clear error message when the limit is exceeded, rather than letting the overflow crash a worker process.

All tools run in your browser. Your data never leaves your device. Last updated: 2026-05-05.