This error occurs when a function calls itself recursively without a proper base case or termination condition, exhausting the call stack. Stack overflow can also happen during deep object operations, circular event listeners, or when processing deeply nested data structures.
Fixes RANGE_ERROR
RangeError: Maximum call stack size exceeded
at calculateFactorial (script.js:5:3)
at calculateFactorial (script.js:6:3)
at calculateFactorial (script.js:6:3)
at calculateFactorial (script.js:6:3)
...repeated thousands of times...RangeError: Maximum call stack size exceededat calculateFactorial (script.js:5:3)at calculateFactorial (script.js:6:3)at calculateFactorial (script.js:6:3)at calculateFactorial (script.js:6:3)...repeated thousands of times...RangeError: Maximum call stack size exceeded (infinite recursion)
A stack overflow happens when the call stack (a limited memory area that tracks function calls) becomes full. Every time a function is called, a new frame is added to the stack. In Node.js, there is a hard limit to how many frames can exist at once (typically around 10,000-30,000 depending on the system). When this limit is exceeded, the JavaScript engine throws a RangeError. This usually indicates infinite recursion where a function keeps calling itself without a way to stop, but can also result from deeply nested callback chains, circular event emissions, or operations on very large/deeply nested objects. The stack overflow indicates a bug in your code logic rather than a system resource shortage.
Look at the error stack trace carefully. Stack overflow errors typically show the same function name repeated many times:
RangeError: Maximum call stack size exceeded
at calculateFactorial (script.js:5:3)
at calculateFactorial (script.js:6:3)
at calculateFactorial (script.js:6:3)
at calculateFactorial (script.js:6:3)
...repeated thousands of times...The function name that repeats is your culprit. In this example, it's calculateFactorial. Run your script with increased logging to trace when the recursion starts:
let callCount = 0;
function calculateFactorial(n) {
callCount++;
console.log(`Call #${callCount}: n=${n}`);
if (callCount > 100) {
console.log('Stack overflow detected, exiting early');
process.exit(1);
}
return calculateFactorial(n - 1);
}Every recursive function MUST have a base case—a condition that stops the recursion. Without it, the function calls itself infinitely.
Before (infinite recursion):
function countdown(n) {
console.log(n);
return countdown(n - 1); // Always calls itself!
}
countdown(5); // RangeError: Maximum call stack size exceededAfter (with base case):
function countdown(n) {
if (n === 0) {
console.log('Done!');
return; // Base case: stop recursion
}
console.log(n);
return countdown(n - 1);
}
countdown(5); // Logs: 5, 4, 3, 2, 1, Done!The base case is the if (n === 0) condition. It's the "stopping point" that prevents infinite recursion.
Recursion consumes stack space for each call. For large datasets, iteration is safer and faster:
Recursive (risky for large arrays):
function sumArray(arr, index = 0) {
if (index === arr.length) {
return 0;
}
return arr[index] + sumArray(arr, index + 1);
}
const largeArray = new Array(50000).fill(1);
sumArray(largeArray); // RangeError: stack overflowIterative (safe for large arrays):
function sumArray(arr) {
let total = 0;
for (let i = 0; i < arr.length; i++) {
total += arr[i];
}
return total;
}
const largeArray = new Array(50000).fill(1);
console.log(sumArray(largeArray)); // Works fine: 50000Loops don't add stack frames, so they handle arbitrarily large datasets without overflow.
If you're emitting an event inside its own listener, you create infinite recursion:
Before (infinite loop):
const { EventEmitter } = require('events');
const emitter = new EventEmitter();
emitter.on('data', (value) => {
console.log(value);
emitter.emit('data', value - 1); // Emits itself!
});
emitter.emit('data', 5); // RangeErrorAfter (with base case):
const { EventEmitter } = require('events');
const emitter = new EventEmitter();
emitter.on('data', (value) => {
console.log(value);
if (value > 0) {
emitter.emit('data', value - 1);
}
});
emitter.emit('data', 5); // Logs: 5, 4, 3, 2, 1, 0Add the termination condition BEFORE emitting the event again.
JSON.stringify() can fail on deeply nested or circular objects:
Risky code:
const obj = { value: 1 };
obj.self = obj; // Circular reference!
JSON.stringify(obj); // RangeError: Maximum call stack size exceededSolution 1: Use a replacer function:
const seen = new WeakSet();
const json = JSON.stringify(obj, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return '[Circular]'; // Skip circular refs
}
seen.add(value);
}
return value;
});Solution 2: Clone objects safely:
function deepClone(obj, depth = 100) {
if (depth === 0) {
console.warn('Max depth reached, truncating object');
return null;
}
if (typeof obj !== 'object' || obj === null) return obj;
const cloned = Array.isArray(obj) ? [] : {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = deepClone(obj[key], depth - 1);
}
}
return cloned;
}Node.js allows you to increase the stack size temporarily, but this is NOT a fix—only a diagnostic tool:
node --stack-size=2000 script.jsThe --stack-size flag sets stack size in kilobytes. This might allow your script to run slightly longer, helping you see where the infinite recursion occurs:
// Add this at the top of your script
let callCount = 0;
const originalFunc = someFunction;
someFunction = function(...args) {
callCount++;
if (callCount % 1000 === 0) {
console.log(`Made ${callCount} calls so far...`);
}
return originalFunc(...args);
};Use this to identify where the recursion starts spinning, but always fix the underlying logic issue. Increasing stack size is a bandage, not a solution.
Tail call optimization:
Some languages optimize "tail recursion" (where the recursive call is the last statement). Unfortunately, Node.js/V8 does NOT perform tail call optimization, so even tail-recursive functions will accumulate stack frames. For Node.js, always prefer iteration over recursion for performance-critical loops.
Asynchronous recursion:
Async functions and Promises can also cause stack issues if chained deeply without proper handling:
// Risky: deeply chained promises
function recursivePromise(n) {
if (n === 0) return Promise.resolve();
return Promise.resolve().then(() => recursivePromise(n - 1));
}
recursivePromise(10000); // Can cause stack overflowUse iteration with async/await instead:
async function iterativeLoop(count) {
for (let i = 0; i < count; i++) {
await somethingAsync();
}
}Worker threads for heavy computation:
If you have legitimate deep recursion that can't be refactored to iteration, consider moving it to a Worker thread, which has its own separate stack space.
Debugging tools:
- Use console.trace() to print the call stack at any point
- Enable V8 code coverage to see which functions are called most frequently
- Use Node.js profiler: node --prof script.js followed by node --prof-process to analyze hot functions
- Use the --abort-on-uncaught-exception flag to generate core dumps for analysis