Lesson 2 · 12 min read

The Call Stack

JavaScript can only ever do one thing at a time. This is the structure that decides exactly what runs right now, what's paused and waiting its turn, and what happens the moment a function calls itself one time too many.

On this page

  1. Concept
  2. Why this matters
  3. The receptionist's tray — an analogy
  4. The two rules: push and pop
  5. What actually sits on the stack
  6. Real example — traced line by line
  7. Same function, separate calls
  8. Stack overflow — when the tray runs out
  9. Reading a real stack trace
  10. See it live — terminal & DevTools
  11. Common misconceptions
  12. Interview questions

Concept

JavaScript is single-threaded. A thread is one path of work that a program follows, one step at a time. Single-threaded means JavaScript has only one such path: one worker, one task, one moment. But your code is full of functions that call other functions. So how does the engine always know which piece of code is running right now, and where to go back to when it is done?

The answer is a structure called the Call Stack. The call stack is a stack (a list where you only add and remove items at the top) that records which Execution Context is running now and which ones are waiting. An Execution Context (Lesson 1) is the container the engine creates to run a piece of code. Every context is placed on this stack, and the engine only ever runs the one on top.

“Stack” here means exactly what it means with a stack of plates. You can only add a plate to the top, and you can only take the top plate off. You cannot pull one out from the middle without moving everything above it. JavaScript's Call Stack follows that same rule.

Why This Matters

  • It's why synchronous code always finishes completely, top to bottom, before the next unrelated line even gets a chance to start.
  • It's directly responsible for the famous Maximum call stack size exceeded error. This error is commonly called a “stack overflow.”
  • It's what lets you read an error's stack trace and know exactly which function called which, in what order, right up to the crash.
  • It's the foundation for the Event Loop, much later in this course. The event loop is a mechanism that watches the call stack and, when the stack is empty, moves the next waiting task onto it. Its whole job depends on knowing whether the stack is empty.

The Receptionist's Tray — An Analogy

One receptionist, one tray

A hotel has one receptionist on the desk — one worker, one task at a time, exactly like JavaScript. She keeps a tray on her desk for index cards. The tray is the call stack and each card is one Execution Context.

Her shift starts → she drops in a card: “Run the Front Desk Today.” It sits at the very bottom, and stays there her whole shift.

A guest calls to book a room → she writes “Book Room” and places it on top of the tray. She stops whatever she was doing and works on this new card immediately.

Mid-booking she needs to confirm a room is free → she writes “Check Availability” and places it on top of “Book Room.” She works on this first, because it's now the topmost card.

The room checks out free → she removes “Check Availability” from the tray entirely and goes straight back to “Book Room” — exactly where she paused.

Booking confirmed → she removes “Book Room” too. Back to “Run the Front Desk Today,” at the bottom, where she stays until the shift ends.

Run the Front Desk Today
1. shift starts
Run the Front Desk Today
Book Room
2. guest calls
Run the Front Desk Today
Book Room
Check Availability
3. confirm it's free
Run the Front Desk Today
Book Room
4. room is free
Run the Front Desk Today
5. booking confirmed
The tray over one booking. Cards only ever go on top and only ever come off the top. · analogy

She only ever works on the card sitting on top. She never reaches underneath while something sits above it. That single rule — top card only — is the entire Call Stack.

The Two Rules: Push and Pop

Everything the Call Stack does comes down to exactly two operations:

  • Push — whenever a function is called, a fresh Execution Context is created for it and placed on top of the stack. Nothing below it is touched; it just waits, paused, underneath.
  • Pop — the instant a function finishes (its last line runs, or it hits return), its Execution Context is removed from the top, and control goes straight back to whichever context is now exposed on top.

Together, push and pop make the stack LIFO — Last In, First Out. Whatever was pushed most recently is always the first thing to get popped.

Global EC
running
1. program starts
push bookRoom ↗
Global EC
bookRoom EC
running
2. bookRoom called
push checkAvailability ↗
Global EC
bookRoom EC
checkAvailability EC
running
3. checkAvailability called
pop ↘
Global EC
bookRoom EC
running
4. checkAvailability done
pop ↘
Global EC
running
5. bookRoom done
Global goes on first and comes off last. Whatever is pushed most recently is always popped first — that's LIFO. · push / pop
Stack vs. queue, in one line

A stack is Last In, First Out (the newest item leaves first, like a pile of plates). A queue is First In, First Out (the oldest item leaves first, like people waiting in a line). The call stack is a stack. Lesson 14 introduces a queue.

What Actually Sits On The Stack

A quick recap from Lesson 1, because it matters here: Execution Context (EC) is the general term for the container that runs a piece of code. Global Execution Context (GEC) is just one specific EC — the first one, created automatically, sitting at the very bottom. Every function call creates another specific kind, a Function Execution Context, which gets pushed above whatever's already there.

GEC is pushed exactly once, automatically, right when the program starts — and it is the very last thing popped, only once the entire program has nothing left to run.

Real Example — Traced Line By Line

stack.js
var hotelName = "Grand Palace";

function bookRoom(guestName) {
  var roomNumber = 101;
  checkAvailability(roomNumber);
  console.log(guestName + " booked room " + roomNumber);
}

function checkAvailability(room) {
  var isFree = true;
  console.log("Room " + room + " available: " + isFree);
}

bookRoom("Aditi");
step 1 / 11
Setup
Program starts — Global EC pushed

The engine creates the Global Execution Context and pushes it onto the empty stack. Memory phase: hotelName → undefined, and both functions stored in full.

stack.js
1var hotelName = "Grand Palace";
2
3function bookRoom(guestName) {
4 var roomNumber = 101;
5 checkAvailability(roomNumber);
6 console.log(guestName + " booked room " + roomNumber);
7}
8
9function checkAvailability(room) {
10 var isFree = true;
11 console.log("Room " + room + " available: " + isFree);
12}
13
14bookRoom("Aditi");
Call stack · top is running
Global ECrunning
hotelNameundefined
bookRoom{ …code }
checkAvailability{ …code }
Console
(nothing printed yet)
Step through the program. Every call pushes a fresh context on top; every finish pops it and returns control to the one underneath — exactly where it paused. · interactive
  1. Global EC pushed

    Stack: [ Global ]. Memory phase reserves hotelName → undefined, plus both functions in full. Code phase runs: hotelName = "Grand Palace", then bookRoom("Aditi") is called.

  2. bookRoom EC pushed

    Stack: [ Global, bookRoom ]. Its own private memory: guestName → "Aditi", roomNumber → undefined. Code runs: roomNumber = 101, then checkAvailability(101) is called.

  3. checkAvailability EC pushed

    Stack: [ Global, bookRoom, checkAvailability ]. Its own private memory: room → 101, isFree → undefined. Code runs: isFree = true, then prints Room 101 available: true.

  4. checkAvailability finishes → popped

    Stack: [ Global, bookRoom ]. Its memory is discarded completely. Control returns to bookRoom, right after the call, which prints Aditi booked room 101.

  5. bookRoom finishes → popped

    Stack: [ Global ]. Program has nothing left → Global is popped too. Stack: [ ].

Same Function, Separate Calls

It isn't only different functions that get different ECs — the same function called twice gets two completely separate, brand-new ECs that share nothing.

separate.js
function bookRoom(guestName) {
  var roomNumber = Math.floor(Math.random() * 100);
  console.log(guestName + " got room " + roomNumber);
}

bookRoom("Aditi");
bookRoom("Rahul");

1st call → fresh EC → its own roomNumber, say 42 → prints Aditi got room 42 → popped, thrown away entirely.
2nd call → a brand-new, separate EC (never the reused first one) → its own roomNumber, say 77 → prints Rahul got room 77 → popped.

1st call · bookRoom("Aditi")
  • guestName"Aditi"
  • roomNumber42
popped · gone
2nd call · bookRoom("Rahul")
  • guestName"Rahul"
  • roomNumber77
popped · gone
shares
nothing
⟷
Two calls, two contexts, two separate memories. The second one is built from scratch after the first is already gone. · separate calls

Even though it's the exact same function, Aditi's card and Rahul's card never see each other's sticky note.

Stack Overflow — When The Tray Runs Out

The tray isn't infinite — the stack has a fixed amount of memory set aside for it. The limit depends on the engine and on how big each call is. In Node.js, a very small function usually reaches it after roughly ten thousand calls. Recursion means a function that calls itself. If a function keeps calling itself with no stopping point, cards keep getting pushed, one after another, with nothing popped in between:

overflow.js
function countDown(n) {
  console.log(n);
  countDown(n - 1); // never stops — no condition to stop it
}

countDown(5);

Each call is still waiting on the next one to finish before it can finish itself, so nothing ever gets popped — the tray just keeps growing. Eventually there is no more room, and JavaScript throws a RangeError (the error type JavaScript uses when a value or size is outside the allowed range):

RangeError: Maximum call stack size exceeded

step 1 / 6
Push · nothing ever pops
countDown(5) called

Global calls countDown(5). One frame pushed. It prints 5, then on line 3 calls countDown(4) — and now it has to wait for that call to finish before it can finish itself.

overflow.js
1function countDown(n) {
2 console.log(n);
3 countDown(n - 1); // never stops
4}
5
6countDown(5);
Console
› 5
Call stack1 frames
stack memory used5%
Global EC
countDown(5) ECrunning
Nothing ever pops, so the stack fills up until it has nowhere left to grow. · interactive

This is exactly why every recursive function needs a base case — a condition that stops it from calling itself further, so the stack can start popping back down. A working version: function countDown(n) { if (n < 0) return; console.log(n); countDown(n - 1); }

Reading A Real Stack Trace

When an error is thrown, JS prints a stack trace. A stack trace is a printed list of what was on the Call Stack at the moment the error happened. Each line is one frame, which means one function call that was still waiting.

console
Uncaught RangeError: Maximum call stack size exceeded
    at countDown (index.js:3)
    at countDown (index.js:3)
    at countDown (index.js:3)
    at countDown (index.js:3)
    ...

Read it top to bottom: the top line is exactly where you were standing the instant it broke — the most recent, deepest call. Each line below it is one card further down the tray, walking back toward Global at the very bottom (Chrome and Node.js, which both use the V8 engine, show at most 10 frames by default, because there could be thousands).

Console
Uncaught RangeError: Maximum call stack size exceeded
at countDown (index.js:3)
at countDown (index.js:3)
at countDown (index.js:3)
at countDown (index.js:3)
...
(not shown)
⟶
⟶
⟶
⟶
⟶
⟶
Call stack at the crash
countDown(−11,414) EC
top · where it broke
countDown(−11,413) EC
one below
countDown(−11,412) EC
countDown(−11,411) EC
⋮ thousands more ⋮
printout cut off
Global EC
bottom
Read a trace top to bottom. The first line is the top of the stack — the deepest call, the instant it broke. Each line below is one card further down the tray. · stack trace ↔ stack

See It Live — Terminal & DevTools

Option A — Node.js debugger

  1. Save the file

    Save the hotelName / bookRoom example from earlier as stack.js.

  2. Launch with the inspector

    The inspector is Node's built-in debugging connection. The -brk part makes Node pause before the first line.

    terminal
    node --inspect-brk stack.js
  3. Attach Chrome

    Open chrome://inspect, click inspect under Remote Target.

  4. Step into every call

    Use Step Into (not Step Over) on bookRoom(...) and checkAvailability(...). Step Into means “go inside the function that is called on this line.” Step Over means “run the whole call and stop at the next line.”

  5. Watch the Call Stack panel

    Watch a new frame appear every time you step into a call, and disappear the instant that function returns — this panel is the tray, shown live.

Option B — Trigger an overflow directly

  1. Open the console

    Any page → DevTools (F12) → Console tab.

  2. Paste the broken countDown

    The version with no base case, from the Stack Overflow section above.

  3. Run it

    Watch the console print RangeError: Maximum call stack size exceeded along with a (truncated) stack trace — real proof the tray ran out of room.

Common Misconceptions

“The call stack and the memory heap are the same thing.” They're not. The heap is a separate, large area of memory where objects and functions are stored. The call stack only tracks the order of what is running now. It is a to-do tray, not a warehouse.

“Recursion is dangerous and should be avoided.” Recursion itself is completely normal and used constantly. The danger is only recursion with no base case, or one that really needs more depth than the stack can hold.

“The stack keeps a record of every function that ever ran.” The opposite is true. The moment an EC is popped, it and its memory are thrown away completely. Nothing is kept afterward (except by a closure, Lesson 10).

Interview Questions

What is the Call Stack, and why does it matter that JS is single-threaded?

The call stack is a stack (a list where you add and remove only at the top) that tracks which Execution Context is running now. Because JS is single-threaded (it has only one path of work), only one context can be on top and running at a time.

What are the two operations on the stack, and when does each happen?

Push, when a function is called; pop, the instant that function finishes.

Why is it called LIFO, and how does that show up in real code?

Last In, First Out — whichever function was called most recently is always the first one to finish and get removed, before anything below it can continue.

What causes “Maximum call stack size exceeded,” and what's this commonly called?

Pushing more Execution Contexts than the stack has room for, usually from recursion (a function calling itself) with no stopping point. This is commonly called a stack overflow.

If the same function is called 3 separate times, how many Execution Contexts are created in total?

Three — completely separate ones, none of them sharing memory with each other.

What determines exactly when an Execution Context gets popped?

The moment its function finishes running — hits its last line, or a return statement.

How do you read the order of a printed stack trace?

Top to bottom: the top line is the most recent, deepest call at the moment of the crash; each line below walks back toward the Global Execution Context.

Does the Global Execution Context ever get popped, and when?

Yes — but only once, right at the very end, after every other context has already finished and the program has nothing left to run.

Published by Jainik · Last updated · How these lessons are made

Keep learning

Part of the study guide

Related series