completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Stack limits & stack overflow

Learn why deep recursion throws stack overflow errors, how stack depth varies by frame and host, and how explicit stacks avoid crashes.

By the end, you can
  • 01
    Explain the limitExplain why every call that hasn't finished uses stack space, and why that space has a limit.
  • 02
    Read stack overflow errorsRecognize the RangeError from Chrome, Node, and Safari, and Firefox's InternalError: too much recursion.
  • 03
    Avoid the crashCount how deep calls go without trusting one exact number, and turn deep recursion into a loop with an explicit stack.

Why recursion can crash

A recursive function is a function that calls itself. It can call itself directly, or through other functions. The Recursion and Advanced recursion lessons show when this is useful. This lesson is about a limit. If the calls go too deep, the program crashes.

When a function calls another function, the first one is not finished yet. It waits. JavaScript keeps a small record for each waiting call. This record is called a stack frame. It holds the call's values and the place to go back to when the call ends. All the frames sit on the call stack, one on top of another. The Stack frames & calling conventions lesson shows what a frame holds.

Definition

A stack overflow happens when the waiting calls need more stack space than there is. In JavaScript, it usually comes from a recursive function that calls itself too many times, or never stops. The program throws an error before the calls can finish.

The stack space holds four dive() calls, one on top of another. It is full. One more dive() call tries to get in, but there is no room, so the program stops with a stack overflow.stack spacedive()dive()dive()dive()dive()one more callstack overflowno room left
Each dive() call waits for the next one, so the calls pile up. When the stack space is full, the next call has no room. That is a stack overflow.

The amount of stack space is set by the host. The host is the program that runs your JavaScript, such as a browser or Node. Inside each host is an engine: the part that runs the code. Chrome and Node use the V8 engine. Firefox uses SpiderMonkey. Safari uses JavaScriptCore.

The official rulebook for JavaScript is called ECMAScript. It says how calls and returns work. It does not say how much stack space there must be. It also does not say which error to throw when the space runs out. Each engine decides that.

Step through a tiny counter that calls itself
Step 0 of 10Ready
Your turn: follow the blue line

Step through a tiny recursion that stops after three calls. Watch the counter grow with each call. Code without a guard keeps going until the stack is full.

Running in
  1. script
Next: line 11
function countUntilLimit(limit) {  let depth = 0;  function dive() {    depth += 1;    if (depth === limit) return depth;    return dive();  }  return dive();} 
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

The step-through above is tiny and safe. It stops after three calls. Line 2 sets a counter, depth, to 0. Line 4 adds 1 to it. Line 6 calls dive() again, so one more call is now waiting. Line 5 is a guard: a check that stops the calls. Without a guard, the calls keep piling up until the host has no room for another frame.

The limit is space, not a number of calls

The error message says "Maximum call stack size exceeded". That sounds like a fixed number of calls. It isn't. Most engines limit the space on the stack, counted in bytes. A byte is a tiny unit of memory.

  • If each frame is small, more calls fit.
  • If each frame is large, fewer calls fit.
Real-life analogyA stack of plates on a kitchen shelf

Picture plates stacked on a kitchen shelf. The shelf has a fixed height. Small plates make a tall stack. Big plates reach the top sooner.

In real life: The shelf has a fixed height
In JavaScript: The host gives a fixed amount of stack space
In real life: Each plate takes up some height
In JavaScript: Each waiting call needs a frame
In real life: Big plates mean fewer plates fit
In JavaScript: A call with more parameters and variables has a bigger frame, so fewer calls fit
In real life: Another shelf can have a different height
In JavaScript: Another browser, device, or Node setting can give a different amount of space
Two boxes of stack space, both the same size. Left: small frames from a dive() with no parameters. Six fit. Right: large frames from a dive() with 26 parameters. Only two fit.small framesdive()dive()dive()dive()dive()dive()more calls fitlarge framesdive(a, …, z)dive(a, …, z)fewer calls fit
Both boxes are the same size. A call with many parameters needs a bigger frame, so fewer of those calls fit before the space runs out.

The table answers four questions about the limit. Read each row from left to right.

What the stack limit depends on
QuestionAnswerA common mistake
What has a limit?The amount of stack space, in bytes. The host sets it.Thinking JavaScript sets a fixed number of calls.
Why does the depth change?Each frame holds the place to go back to, the parameters, the local variables, and notes for the engine. Bigger frames fit fewer times.Thinking every call takes the same space.
What makes hosts differ?The engine, the device, the setup of the browser or a worker, and Node settings.Thinking a number you measured is true everywhere. It is only true for that host, at that time.
How do I change it in Node?node --stack-size=<kB> sets how much stack space V8 gets.Writing tests that expect one exact depth. Compare numbers instead.

In Node, you can change the amount of stack space with a setting: node --stack-size=<kB>. The number is in kilobytes (kB). A kilobyte is about a thousand bytes. This setting is good for experiments. It is not a real fix for code that recurses as deep as the data a user sends.

Chrome says RangeError, Firefox says InternalError

Each engine reports a stack overflow in its own way:

  • In Chrome and Node (the V8 engine), the error is a RangeError. Its message is Maximum call stack size exceeded.
  • In Safari (JavaScriptCore), MDN shows the same RangeError message, with a full stop at the end.
  • In Firefox (SpiderMonkey), MDN shows InternalError: too much recursion.
Two consoles. The Chrome console shows RangeError: Maximum call stack size exceeded. The Firefox console shows InternalError: too much recursion.Chrome consoleRangeError:Maximum call stacksize exceededChrome, Node, SafariFirefox consoleInternalError:too much recursionFirefox
The problem is the same: the stack space ran out. Chrome, Node, and Safari call it a RangeError. Firefox calls it an InternalError.

InternalError is not one of the standard errors in the ECMAScript rulebook. Only Firefox has it. So code that should work in every browser must not expect InternalError to exist.

The table lists each engine, the type of error it throws, and the message you will likely see.

Stack overflow errors in each engine
EngineError typeMessage you will likely see
V8 (Chrome, Node)RangeErrorMaximum call stack size exceeded
JavaScriptCore (Safari)RangeErrorMaximum call stack size exceeded. MDN shows it with a full stop at the end.
SpiderMonkey (Firefox)InternalErrortoo much recursion. InternalError is not a standard error in the ECMAScript rulebook.
The ECMAScript rulebookIt doesn't name one.The rulebook says how calls work. Each engine decides how to report a full stack.
A depth test for Node, used by the lesson's testsJavaScript
function measureSmallFrame() {  let depth = 0;  function dive() {    depth += 1;    return dive();  }  try {    dive();  } catch (error) {    return { depth, name: error.name, message: error.message, range: error instanceof RangeError };  }} function measureLargeFrame() {  let depth = 0;  function dive(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z) {    const localSum = a + b + c + d + e + f + g + h + i + j + k + l + m + n + o + p + q + r + s + t + u + v + w + x + y + z;    depth += 1;    if (localSum === -1) throw new Error("unreachable");    return dive(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z);  }  try {    dive(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1);  } catch (error) {    return { depth, name: error.name, message: error.message, range: error instanceof RangeError };  }} console.log(JSON.stringify({  node: process.versions.node,  v8: process.versions.v8,  small: measureSmallFrame(),  large: measureLargeFrame(),}));

This code has two functions. Each one calls itself until the stack is full. Line 4 and line 18 add 1 to a counter on every call. When the error comes, line 10 and line 25 save the counter, the error's name and message, and whether it is a RangeError. Line 29 prints all of it as text, so the tests can read it.

Checked in Node

The lesson's test runs this code in Node 22, which uses V8 version 12.4. It runs it twice, with two different --stack-size settings. It does not check exact numbers. It checks three facts:

  • A smaller --stack-size gives fewer calls.
  • The function with large frames makes fewer calls than the function with small frames.
  • The error is a RangeError with the message Maximum call stack size exceeded.

The browser messages in the table come from the MDN page InternalError: too much recursion. MDN is a well-known reference site for web developers. The facts about Node and V8 are checked by the lesson's test.

Measure the depth, but don't trust one number

You can count how many calls fit before the error. This count is the depth. A depth count helps you understand a problem. It tells you what happened in this host, for this function, right now. Don't copy the number into your app as a fixed limit.

The playground below runs two real tests in your browser:

  • The small-frame function only adds 1 to a counter. Then it calls itself.
  • The large-frame function takes 26 parameters and adds them up. Then it calls itself.

Expect two different numbers. Don't expect any exact value.

Count the calls your browser allows
The two test functions the playground runsJavaScript
function measureSmallFrame() {  let depth = 0;  function dive() {    depth += 1;    return dive();  }  try {    dive();  } catch (error) {    return { depth, name: error.name, message: error.message };  }} function measureLargeFrame() {  let depth = 0;  function dive(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z) {    const localSum = a + b + c + d + e + f + g + h + i + j + k + l + m + n + o + p + q + r + s + t + u + v + w + x + y + z;    depth += 1;    if (localSum === -1) throw new Error("unreachable");    return dive(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z);  }  try {    dive(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1);  } catch (error) {    return { depth, name: error.name, message: error.message };  }}
Your browser, right nownot run

Nothing counted yet. Press the button once. Reset clears the result.

Step 0 of 1ready

Press Measure my browser. It counts calls until your browser throws the error, for a small frame and for a large one.

This runs two real functions in your browser tab. Each one calls itself until the stack is full. The numbers are only true here: the engine, the device, and the frame size all change them.

Line 4 adds 1 to the counter in the small function. Line 18 adds 1 to a separate counter in the large function. Both functions use try...catch. It runs the code in try. If that code throws an error, JavaScript jumps to the catch part instead of crashing. So the page can show the numbers, and the lesson keeps working.

Don't put the number in your app

Say your browser reports 12000 calls today. Another browser, phone, or browser version can report a different number. So can a worker, a script that runs in the background. Use the number to understand the risk. Don't use it as a limit in real code.

Bigger frames mean fewer calls

The Node test gives both functions the same amount of stack space. The small function has almost nothing in its frame. The large function has 26 parameters and a local variable. Each large call uses more bytes, so the large function runs out of space sooner.

So a claim like "my browser handles about N calls" is not complete. You also need to know which function made N calls, and in which host.

The rule to remember

The test does not prove that Node allows a certain number of calls. It proves two comparisons:

  • Less stack space means fewer calls.
  • With the same space, bigger frames mean fewer calls.

Use a loop when the depth has no limit

Recursion is often the clearest way to walk through data shaped like a tree, where each item can hold more items. It is safe when you know the depth stays small. It is risky when the depth comes from outside your code: from users, from a server, or from imported files.

The next example uses a linked list. A linked list is a chain of objects. Each object has a value and a next property. The next property points to the next object in the chain. The last object has next: null.

A recursive sum crashes on a list 100000 longPop out in the code editor (opens in a new tab)JavaScript
function makeDeepList(depth) {  let node = null;  for (let i = 0; i < depth; i += 1) {    node = { value: 1, next: node };  }  return node;} function sumRecursive(node) {  if (node === null) return 0;  return node.value + sumRecursive(node.next);} const deepList = makeDeepList(100000);console.log(sumRecursive(deepList));

Line 14 builds a list of 100000 objects. Line 15 calls the recursive sum. Line 11 is the problem. It calls sumRecursive again for the next object, before the current call can finish. So every object in the list adds one more waiting call.

The same sum with a loop and a to-do arrayPop out in the code editor (opens in a new tab)JavaScript
function makeDeepList(depth) {  let node = null;  for (let i = 0; i < depth; i += 1) {    node = { value: 1, next: node };  }  return node;} function sumWithExplicitStack(root) {  let total = 0;  const pending = [root];  while (pending.length > 0) {    const node = pending.pop();    if (node === null) continue;    total += node.value;    pending.push(node.next);  }  return total;} const deepList = makeDeepList(100000);console.log(sumWithExplicitStack(deepList));

This version uses a loop instead of more calls:

  1. Line 11 makes pending, an array of objects still to visit.
  2. Line 13 takes the last object off the array with pop().
  3. Line 15 adds its value to the total.
  4. Line 16 puts the next object on the array with push().

Only one function call is waiting the whole time, and the program prints 100000. The array lives in the heap. The heap is the large area of memory where JavaScript keeps objects and arrays. It has much more room than the call stack. An array that you add to and take from at the same end is also called a stack. Because your own code manages it, it is called an explicit stack.

Left: the recursive sum adds one sumRecursive call for each list node, so the calls pile up until a stack overflow. Right: the loop version has only one sumWithExplicitStack call. A box called pending holds the next node to visit. It prints 100000.recursive sumsumRecursivesumRecursivesumRecursivesumRecursiveone call per nodestack overflowloop with a listsumWithExplicitStackonly one callnext nodependingprints 100000
The recursive sum adds one call for each node, so a long list overflows the stack. The loop makes only one call. It keeps the next node in the pending array instead.
Step through the loop with a to-do array
Step 0 of 14Ready
Your turn: follow the blue line

Step through the loop version. The pending array is your to-do list. It lives in the heap, and only one call waits on the stack.

Running in
  1. script
Next: line 17
const list = { value: 1, next: { value: 2, next: { value: 3, next: null } } }; function sumWithExplicitStack(root) {  let total = 0;  const pending = [root];   while (pending.length > 0) {    const node = pending.pop();    if (node === null) continue;    total += node.value;    pending.push(node.next);  }   return total;} 
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Real-life analogyA to-do list you can only add to the top of

Picture a to-do list where you only add and remove tasks at the top. You take the top task and do it. If it brings new tasks, you write them on top. You never have to keep tasks in your head.

In real life: Keep every task in your head
In JavaScript: Recursive calls keep the waiting work on the call stack
In real life: Write the tasks on a list
In JavaScript: An array holds the objects still to visit
In real life: Take the top task, then write any new tasks on top
In JavaScript: Pop one object, then push the next one
In real life: You can read the list at any time
In JavaScript: Your code can look at the array at any time

The table compares four ways to walk through deep data. Read each row from left to right.

Recursion or a loop?
WayWhere the waiting work is keptWhen to use it
RecursionOn the call stack, as waiting calls.When the data is shallow. It crashes when the depth is more than the stack can hold.
Loop with an explicit stackIn an array in the heap. You take from the end.When the data can be very deep. It needs a little more code, but only one call waits.
Loop with a queueIn an array in the heap. You take from the front, so you visit one level at a time.When you want to visit items level by level. Watch that the array doesn't grow without end.
Proper tail callThe engine may reuse the current frame.The next lesson covers it. Most engines don't support it today.
Risky or safe?
  • sum(node.next) on a linked list that can be 100000 objects long.
  • A while loop with const pending = [root] to walk through a tree.
  • Running the same test with node --stack-size=256 and with --stack-size=768.
  • A function with many parameters and variables makes fewer calls than a tiny function.
  • Walking through a Bengaluru shop's category tree with an explicit stack of categories to visit.
  • A recursive save function saves part of the data, then catches the RangeError at the end.
Try it yourself
0 of 6 correct

Put each card in a group: a stack overflow risk, a safer way, or a fact that depends on the host.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Where this shows up in real apps

Data nested many levels deep shows up in many places:

  • comment threads, with replies to replies
  • category trees and menus in a shop
  • page trees in a content management system (CMS), the tool a team uses to edit its website
  • file explorers, with folders inside folders
  • JSON files that you import
  • maps of links between pages

For example, a Bengaluru events site loads a comment thread. The replies can nest much deeper than the designer expected.

Count the comments in a Bengaluru thread with a loopPop out in the code editor (opens in a new tab)JavaScript
const thread = {  text: "Bengaluru meetup",  replies: [    { text: "Where in Indiranagar?", replies: [] },    { text: "Can I bring a friend?", replies: [      { text: "Yes, register both names.", replies: [] },      { text: "Metro is easier than parking.", replies: [] },    ] },  ],}; function countComments(root) {  let count = 0;  const pending = [root];  while (pending.length > 0) {    const comment = pending.pop();    if (!comment) continue;    count += 1;    for (const reply of comment.replies) pending.push(reply);  }  return count;} console.log(countComments(thread));

Line 14 puts the first comment in pending. Line 16 takes one comment off. Line 18 counts it. Line 19 puts all its replies on pending. So the deep replies wait in the array, not on the call stack. The example prints 5.

  • Use recursion when you know the depth stays small.
  • Use a loop with an explicit stack or a queue when the depth comes from users, a server, or imported files. A queue is an array where you add at the end and take from the front.
  • Some data can point back to itself in a circle. Check for that in your own code. Don't wait for a stack overflow to stop it.
  • If you change code to make it faster, measure first. The reason for a loop here is safety with deep data, not speed.
  • The Tail calls lesson covers tail calls and trampolines. This lesson doesn't.

Common misconceptions

  • "The limit is the same everywhere." It changes with the host, the stack space setting, the frame size, the device, and how the engine has sped up the code.
  • "A stack overflow means all memory is full." The call stack and the heap are different areas of memory. An explicit stack uses the heap, so the calls don't pile up.
  • "I can catch the RangeError and use it to stop a loop." By the time you catch it, some work has already happened. The stack was almost full when the error was thrown.
  • "All recursion is bad." Recursion is fine when the depth is small and has a limit. The risk is depth with no limit.
  • "Tail calls fix this everywhere." Tail calls and trampolines are the next lesson. Most engines don't support proper tail calls.
Catching the error does not undo work already donePop out in the code editor (opens in a new tab)JavaScript
let writes = 0;function riskySave() {  writes += 1;  return riskySave();} try {  riskySave();} catch (error) {  console.log(error instanceof RangeError);  console.log(writes > 0);}

In V8, this code prints true twice. The first true says the error is a RangeError. The second says writes is already more than 0. Catching the error did not undo line 3.

The table lists four ideas that are easy to mix up. Each row says what the idea means and what it is not.

Ideas that are easy to mix up
IdeaWhat it meansWhat it is not
Stack overflowThe host ran out of stack space for waiting calls.It is not a heap memory leak. It also doesn't mean the idea of the function is wrong.
Recursion depthHow many calls happened before the error.It is not the same in every engine, device, setting, or function.
Catching the errorYou can often catch it with try...catch.It is not a safe way to stop your code, because some work has already happened.
The loop fixKeep the waiting work in your own array or queue.It is not always faster. It is safer when the depth has no limit.

Practice exercises

Exercise 1 · Warm-upPredict what the counter prints

What does this code print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
function countUntilLimit(limit) {
  let depth = 0;
  function dive() {
    depth += 1;
    if (depth === limit) return depth;
    return dive();
  }
  return dive();
}
console.log(countUntilLimit(3));

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upName the error in Node

    What type of error does the Node depth test catch when the stack is full?

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeName Firefox's message

      What short message does Firefox show when recursion goes too deep?

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeFind the problem in the list sum

        What goes wrong when this code runs?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        function makeDeepList(depth) {
          let node = null;
          for (let i = 0; i < depth; i += 1) {
            node = { value: 1, next: node };
          }
          return node;
        }
        
        function sumRecursive(node) {
          if (node === null) return 0;
          return node.value + sumRecursive(node.next);
        }
        
        const deepList = makeDeepList(100000);
        console.log(sumRecursive(deepList));

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · PracticeFix a shop's category tree

          On a shopping site, sellers can put categories inside categories, many levels deep. You walk through them with a loop. What should hold the categories you still have to visit?

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeCompare small and large frames

            Both functions get the same stack space. Which one makes fewer calls: the one with a small frame, or the one with a large frame?

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, ask two things. Is this a JavaScript rule, or something the host decides? And where is the waiting work kept?

              Stack overflow quiz · 8 questionsScore: first tries count
              1. Question 1 of 8What does 'maximum call stack size' really mean?

                Choose an answer to see the explanation.

              2. Question 2 of 8What does this counter with a guard print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                function countUntilLimit(limit) {
                  let depth = 0;
                  function dive() {
                    depth += 1;
                    if (depth === limit) return depth;
                    return dive();
                  }
                  return dive();
                }
                console.log(countUntilLimit(3));

                Choose an answer to see the explanation.

              3. Question 3 of 8In Node 22, with V8 12.4, which fact does the lesson's test check?

                Choose an answer to see the explanation.

              4. Question 4 of 8Which error message does MDN show for Firefox?

                Choose an answer to see the explanation.

              5. Question 5 of 8In the same Node program, why does the large-frame function make fewer calls than the tiny one?

                Choose an answer to see the explanation.

              6. Question 6 of 8What does this loop version of the list sum print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const list = { value: 1, next: { value: 2, next: { value: 3, next: null } } };
                function sumWithExplicitStack(root) {
                  let total = 0;
                  const pending = [root];
                  while (pending.length > 0) {
                    const node = pending.pop();
                    if (node === null) continue;
                    total += node.value;
                    pending.push(node.next);
                  }
                  return total;
                }
                console.log(sumWithExplicitStack(list));

                Choose an answer to see the explanation.

              7. Question 7 of 8Why is catching a stack overflow RangeError a bad way to stop your code?

                Choose an answer to see the explanation.

              8. Question 8 of 8What should you do with a tree, comment thread, or category list that can be very deep?

                Choose an answer to see the explanation.

              Key takeaways

              • Each waiting call uses some stack space. The space has a limit, so very deep recursion crashes.
              • The limit depends on the host and on the frame size. Never count on one exact depth.
              • Chrome, Node, and Safari throw a RangeError. Firefox throws InternalError: too much recursion.
              • Measure the depth only to understand a problem. Catch the error only to report it, not to stop a loop.
              • For deep trees, lists, and comment threads, keep the waiting work in an explicit stack or a queue.

              One-line summary.
              Don't let deep data turn into deep calls.

              Next: Tail calls explains proper tail calls and trampolines.

              CompleteFrontend Clear concepts. Working examples.