← all labs  ·  ← prev lab  ·  next lab →

Foundation Lab 05

Execution Context & the Call Stack

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
▶
EXECUTION 10 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

A stack of trays, one order per tray Three stacked trays; only the top tray is being worked on. counter order #1 order #2 only top tray is active

Mental Model 1 of 5One Active Tray at a Time

Contents of one tray A box split into three labeled slots. this call's own variables where to resume after link back to the caller

Mental Model 2 of 5What One Tray Holds

New tray added, tray removed when done A hand adds a tray on order, and removes the top tray only once it's finished. new order order done

Mental Model 3 of 5Add on Call, Remove on Return

An analogy: phone calls placed on hold A phone handset with three calls stacked on hold, only the top one connected live. call C — on hold call B — on hold call A — talking

Mental Model 4 of 5An Analogy: Calls on Hold

Misconception: paused does not mean forgotten or still running A red X over a spinning gear labeled still running, a green check over a still snapshot labeled held exactly as left. ✕ "still running" ✓ held, unchanged

Mental Model 5 of 5Misconception: Paused ≠ Still Running

Learning Objectives

  1. 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.
  2. Objective 2 of 4 — I can state precisely what gets pushed onto the stack and what gets popped off, for any single function call.
  3. Objective 3 of 4 — I can explain why two functions can never be "in progress" at the exact same instant in JavaScript.
  4. 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

  1. Before any of your code runs, it builds the global execution context and puts it on the stack first.
  2. Each function call builds a new context for that specific call and puts it on top of the stack.
  3. The engine only ever works on the context at the top — it does not return to whatever called it until that top context finishes.
  4. 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.

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.

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"?

Reflection 2 of 3. Where have you already relied on call-stack behavior without naming it — a debugger's "stack trace" view, for instance?

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?


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.

← All labs  ·  Lab 06 — Creation Phase vs Execution Phase →