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
- Throw an error deep in a chain; paste the stack into comments in your own words.
- Cause a small recursion loop; see the RangeError once, then remove it.
- 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.
- React Server Components explained — see execution boundaries and composition decisions in a modern app
- Node.js error handling — watch call stacks and thrown errors matter in real request code