What you'll be able to do

By the end of this page you should be able to describe execution context and the call stack in plain language, read a simple stack trace top-to-bottom, and see why infinite recursion blows the stack — without memorizing operating-system diagrams.

Previous post: closures with real examples.

Who this is for

  • People who saw "Maximum call stack size exceeded" and only knew " recursion bad "
  • Beginners who want a light mental model before the event loop post
  • Anyone confused by stack traces in DevTools

You can skip this if stack traces already tell you a clear story. The next post adds timers and promises on top of this picture.

One thread, one stack (the simple picture)

JavaScript on the page runs your code on a single call stack — one thing executing at a time until it returns.

function c() { console.log("c"); }
function b() { c(); }
function a() { b(); }
a();

Scenario A — order of logs. a calls b, b calls c, c finishes, b finishes, a finishes. Last in, first out — like a stack of plates.

Scenario B — stack trace in an error. The top line is where you were when things broke; lines below show who called who.

The rule: each function call pushes a frame; each return pops it.

Execution context — what a frame holds (lightly)

An execution context is the environment for one running function: local variables, arguments, and where to return. You do not need to name every internal slot — the useful habit is each call gets its own locals.

function add(x, y) {
  const sum = x + y;
  return sum;
}

Scenario A — two calls, two sums. add(2, 3) and add(10, 1) do not share sum; each frame has its own.

Scenario B — closure connection. An inner function keeps its outer frame's variables alive — that is the closure you met in the last post, seen from the stack side.

Stack overflow — recursion with no base case

function forever() {
  forever();
}
// forever(); // RangeError: Maximum call stack size exceeded

Scenario A — accidental recursion. function handleClick() { handleClick(); } fills the stack instantly.

Scenario B — deep but finite recursion. Tree walks can be deep; if you hit the limit, rewrite with a loop or an explicit queue — not something beginners hit on day one.

We are not dumping OS memory maps here — only the JS habit: synchronous calls nest until they return.

A tiny practice file

<!DOCTYPE html>
<html>
  <body>
    <button id="go">Run stack demo</button>
    <pre id="out"></pre>
    <script>
      function third() {
        document.getElementById("out").textContent = new Error().stack;
      }
      function second() {
        third();
      }
      function first() {
        second();
      }
      document.getElementById("go").addEventListener("click", function () {
        first();
      });
    </script>
  </body>
</html>

Click — read the stack from top (where you are) downward (who called who).

Mistakes I see a lot

1. Reading stack traces bottom-to-top. Start at the top line for the immediate failure site.

2. Assuming two setTimeout callbacks run at once on the stack. They run one after another when their turn comes — the next post explains scheduling.

3. Blaming "the engine is multithreaded" for every bug. Browser JS on your page is single-threaded for your script's call stack.

4. Ignoring stack traces because they look long. Expand only the first few your-code lines.

What to try before the next post

  1. Throw an error deep in a chain; paste the stack into comments in your own words.
  2. Cause a small recursion loop; see the RangeError once, then remove it.
  3. Build the tiny HTML file.

Next in this series: event loop and task queues — what runs after the stack clears.

Try this next outside the series

These mental models become more useful when you can point at a real framework problem they explain.