One function calling another feels instant and simple. Underneath, the engine is managing a
strict, orderly stack of paused work. That stack is what this lab makes visible.
What this lab produces: no files, no commit — a pure concept lab. You'll run
two short snippets in a console or with node.
Concept type: Common / Universal Concept
▶
EXECUTION10 of 10
Every language needs some model for how source code becomes running computation, step by
step. This lab is JavaScript's answer.
Mental Models
Mental Model 1 of 5One Active Tray at a Time
Only the top tray is being worked. Everything below it is real, held, and waiting — not forgotten, not running, just paused.
Mental Model 2 of 5What One Tray Holds
Every call gets a record like this. The engine's name for a tray is an execution context.
Mental Model 3 of 5Add on Call, Remove on Return
A tray only ever leaves the stack from the top, and only once that order is fully done — never before, never out of order.
Mental Model 4 of 5An Analogy: Calls on Hold
Picture juggling phone calls: you put caller A on hold to take caller B, then put B on hold to take caller C. You're only ever actually talking to one person at a time — the others are genuinely on hold, waiting, in the exact order you'll return to them.
Mental Model 5 of 5Misconception: Paused ≠ Still Running
It's tempting to imagine a paused function is somehow still quietly doing something in the background. It isn't — its execution context just sits there, completely frozen, exactly as it was the instant it got paused, until it's back on top of the stack.
Learning Objectives
Objective 1 of 4 — I can trace, for a short chain of function calls, exactly what the call stack looks like at any specific point during execution.
Objective 2 of 4 — I can state precisely what gets pushed onto the stack and what gets popped off, for any single function call.
Objective 3 of 4 — I can explain why two functions can never be "in progress" at the exact same instant in JavaScript.
Objective 4 of 4 — I can explain RangeError: Maximum call stack size exceeded in terms of the stack's finite memory filling up, not just "infinite loop."
Pre-Lab Quiz
Answer from intuition, before any explanation. A "wrong" answer here just tells you exactly
what this lab needs to fix.
Question 1 of 5A function calls another function partway through its own code. What happens to the outer function while the inner one is running?Open question
Question 2 of 5Can JavaScript be in the middle of two different function calls at the exact same instant?Open question
Question 3 of 5Guess: what happens if a function keeps calling itself with no condition that ever stops it?Open question
Question 4 of 5Function A calls function B, which calls function C. While C is running, how many execution contexts are on the stack, including global?Open question
Question 5 of 5Once C finishes and returns, which function's code resumes next, and from where?Open question
The Concept
The call stack itself is not a JavaScript invention
Python, Java, C#, and most mainstream languages/runtimes track active function calls with a
call stack too — it's a general programming-language mechanism, not something JavaScript
came up with. What's specific to JavaScript is the exact shape of the record it keeps for
each call — the execution context — and how that record is built. That's
the actual subject of this lab.
Cross-language note
Common concept: call stack / stack frames.
Python: every Python call pushes a frame too — it's why a
RecursionError exists, the same underlying idea tested in Question 3 above.
JavaScript: same idea, one call stack per thread — what's JavaScript-specific
is what gets stored in each context, next.
Why does an execution context need to exist at all?
When one function calls another, the engine has to answer a very practical question:
once the inner call finishes, exactly where should execution pick back up, and
with which values? Without some record answering that, "returning" from a function call
would be meaningless — the engine would have no way to know what it was doing before, or
what local variables that paused call still needs. An execution context
is that record: a small package the engine builds for every single call, holding the
answer to "where was I, and what did I have."
What an execution context actually is
Whenever JavaScript runs any function — including the top-level code in a file — it first
builds an execution context: a small record tracking that call's own
variables, where to resume once it's done, and a link back to whoever called it (the second
diagram above). Calling a function pushes a new context onto the call stack. Returning from
it pops that same context back off.
What the engine is doing, step by step
Before any of your code runs, it builds the global execution context and puts it on the stack first.
Each function call builds a new context for that specific call and puts it on top of the stack.
The engine only ever works on the context at the top — it does not return to whatever called it until that top context finishes.
When a call finishes, its context comes off the top, and the caller picks up exactly where it left off.
Lab
Primary objective: predict, then observe, the exact order function calls resolve in, and connect it to what the stack looks like at each point.
You'll need: a browser console or node — no project, no files.
Lab 1 of 1 — Trace a small call chain
Task
Two snippets, one small change apart. Predict the print order before running each one.
Instructions
Step 1 of 5
// Paste this into a scratch file or console. Read it — do not run it yet.
function takeOrder() {
console.log("order taken");
brewCoffee();
console.log("order handed to customer");
}
function brewCoffee() {
console.log("coffee brewed");
}
takeOrder();
This step is complete when: you've formed a guess about the print order without running it.
Step 2 of 5
Predict the exact order the three lines print in, then reveal.
Output:
order taken
coffee brewed
order handed to customer
What you wrote: a call to takeOrder(), which itself calls brewCoffee()
partway through.
What the engine effectively does:
1. global context on the stack
2. takeOrder() called -> its context pushed, logs "order taken"
3. brewCoffee() called -> its context pushed, logs "coffee brewed"
4. brewCoffee() finishes -> its context popped
5. back inside takeOrder(), logs "order handed to customer"
6. takeOrder() finishes -> its context popped
takeOrder() cannot reach its last line until brewCoffee() has
completely finished -- the engine never jumps back to a paused
context while something is still sitting on top of it.
This step is complete when: you've run the snippet yourself and it matches the revealed order.
Step 3 of 5
// Edit the file from Step 1 yourself. Don't paste finished code — type it from these comments.
// Add a new function named steamMilk, with no parameters.
// Inside steamMilk, log the string "milk steamed".
// Inside brewCoffee, after its existing console.log line,
// add a call to steamMilk().
// Leave takeOrder() and the final takeOrder(); call unchanged.
This step is complete when: your file calls steamMilk() from inside brewCoffee(), and you can say exactly where that tray would sit relative to the other two.
Step 4 of 5
Predict the full print order, and how many contexts are on the stack (including global) at the moment steamMilk() is running.
Output:
order taken
coffee brewed
milk steamed
order handed to customer
While steamMilk() runs, the stack is 4 deep:
steamMilk -> brewCoffee -> takeOrder -> global
One more level of nesting just adds one more context to the
same stack -- the mechanism is identical, only the depth changes.
Step 5 of 5
Comprehension check — write your answer before moving on: if steamMilk() called itself with no stopping condition, what would eventually happen, and why, specifically in terms of the call stack?
Comprehension Checklist
I can explain why "order handed to customer" can't print before "coffee brewed," without just saying "it runs top to bottom."
I can say precisely what gets added to the stack and what gets removed, for any single function call.
I can explain RangeError: Maximum call stack size exceeded in terms of the stack filling up, not just "infinite loop."
I understand the call stack is a general concept, not something unique to JavaScript.
Post-Lab Quiz
These are different questions from the pre-lab quiz — they test what the experiment should
have taught you, not the same questions again.
Question 1 of 5In the 3-function version, what does the call stack look like at the exact moment steamMilk() is executing its console.log?Open question
Question 2 of 5Why can't brewCoffee() and takeOrder() be "in progress" at the same time?Open question
Question 3 of 5What specifically causes RangeError: Maximum call stack size exceeded?Open question
Question 4 of 5Suppose takeOrder() called brewCoffee() twice in a row (not nested — one call, then it returns, then a second call). At most, how many brewCoffee contexts ever exist on the stack simultaneously?Open question
Question 5 of 5A stack trace lists, top to bottom: steamMilk, brewCoffee, takeOrder, (anonymous). Which function's code is the engine actually executing at that moment?Open question
Reflection Questions
Write your own answer first — the blurred text is one possible answer, not the only correct one.
Reflection 1 of 3. Before this lab, would you have described a paused function as "fully stopped," or more like "still kind of running in the background"?
"Kind of still running" is a common intuition, especially coming from UI frameworks where
things feel like they update live. The lab's model is stricter: a paused context does
nothing at all until it's back on top of the stack — no partial progress, no background
activity, just a held record waiting its turn.
Reflection 2 of 3. Where have you already relied on call-stack behavior without naming it — a debugger's "stack trace" view, for instance?
A stack trace in an error message or debugger is a direct printout of the call stack at
the moment something went wrong — each line is one context, ordered from the innermost
(where the error happened) down to whatever originally kicked things off. That's the
same stack this lab traced by hand, just captured automatically on a crash.
Reflection 3 of 3. Before running Step 4's three-function version, did you correctly predict the stack would be 4 deep (including global), or did you forget to count global? What does that tell you about where your mental model still had a gap?
A common gap is forgetting the global execution context entirely, since it's created
silently before any of your own code runs and never shows up as a function you wrote.
It's still a real context occupying a real slot on the stack the whole time your program
runs — every count of "how deep is the stack" needs to include it.
What Breaks If This Understanding Is Wrong?
If function calls feel like they happen "kind of in parallel" or "whenever they get around to
it," nothing about hoisting, scope, closures, or the event loop later in this curriculum will
make sense — all of them depend on "exactly one thing runs at a time, in a strict add/remove
order."
What JavaScript Concept Was Learned Today?
The call stack (a general programming concept) and the execution context (JavaScript's own
record built for every call) — the foundation the rest of this curriculum builds on.
Relates to: ▶ Execution (10 of 10) — this experiment demonstrated
JavaScript's specific answer to "how does source code become running computation."
Next
Lab 06 opens up an execution context and shows it's actually built in two separate passes.
That two-phase model directly explains "hoisting" — a behavior that looks mysterious until
you know exactly what this lab already taught: how a context comes into existence in the
first place.