docs/concepts/call-stack.mdx
How does JavaScript keep track of which function is running? When a function calls another function, how does JavaScript know where to return when that function finishes?
The answer is the call stack. It's JavaScript's mechanism for tracking function execution.
function greet(name) {
const message = createMessage(name)
console.log(message)
}
function createMessage(name) {
return "Hello, " + name + "!"
}
greet("Alice") // "Hello, Alice!"
When greet calls createMessage, JavaScript remembers where it was in greet so it can return there after createMessage finishes. The call stack is what makes this possible.
Imagine you're working in a restaurant kitchen, washing dishes. As clean plates come out, you stack them one on top of another. When a server needs a plate, they always take the one from the top of the stack, not from the middle or bottom.
┌───────────┐
│ Plate 3 │ ← You add here (top)
├───────────┤
│ Plate 2 │
├───────────┤
│ Plate 1 │ ← First plate (bottom)
└───────────┘
This is exactly how JavaScript keeps track of your functions! When you call a function, JavaScript puts it on top of a "stack." When that function finishes, JavaScript removes it from the top and goes back to whatever was underneath.
This simple concept, adding to the top and removing from the top, is the foundation of how JavaScript executes your code.
The call stack is a mechanism that JavaScript uses to keep track of where it is in your code. Think of it as JavaScript's "to-do list" for function calls, but one where it can only work on the item at the top.
function first() { second(); }
function second() { third(); }
function third() { console.log('Hello!'); }
first();
// Stack grows: [first] → [second, first] → [third, second, first]
// Stack shrinks: [second, first] → [first] → []
The call stack follows a principle called LIFO: Last In, First Out.
LIFO = Last In, First Out
┌─────────────────┐
│ function C │ ← Last in (most recent call)
├─────────────────┤ First to finish and leave
│ function B │
├─────────────────┤
│ function A │ ← First in (earliest call)
└─────────────────┘ Last to finish
JavaScript is single-threaded, meaning it can only do one thing at a time. According to the ECMAScript specification, each function invocation creates a new execution context that gets pushed onto the stack. The call stack helps JavaScript:
Let's trace through a simple example to see the call stack in action.
function greet(name) {
const greeting = createGreeting(name);
console.log(greeting);
}
function createGreeting(name) {
return "Hello, " + name + "!";
}
// Start here
greet("Alice");
console.log("Done!");
```
Call Stack: [ empty ]
```
```
Call Stack: [ greet ]
```
Now JavaScript enters the `greet` function and starts executing its code.
```
Call Stack: [ createGreeting, greet ]
```
Notice: `greet` is **paused** while `createGreeting` runs. JavaScript can only do one thing at a time!
```
Call Stack: [ greet ]
```
The return value (`"Hello, Alice!"`) is passed back to `greet`.
```
Call Stack: [ empty ]
```
**Output:**
```
Hello, Alice!
Done!
```
┌─────────┐ ┌─────────┐ ┌────────────────┐ ┌─────────┐ ┌─────────┐
│ (empty) │ → │ greet │ → │createGreeting │ → │ greet │ → │ (empty) │
└─────────┘ └─────────┘ ├────────────────┤ └─────────┘ └─────────┘
│ greet │
└────────────────┘
Program greet() createGreeting() createGreeting greet()
starts called called returns returns
```
When we say a function is "on the stack," what does that actually mean? Each entry on the call stack is called an execution context, sometimes referred to as a stack frame in general computer science terms. It contains everything JavaScript needs to execute that function.
<AccordionGroup> <Accordion title="Function Arguments"> The values passed to the function when it was called.```javascript
function greet(name, age) {
// Arguments: { name: "Alice", age: 25 }
}
greet("Alice", 25);
```
```javascript
function calculate() {
const x = 10; // Local variable
let y = 20; // Local variable
var z = 30; // Local variable
// These only exist inside this function
}
```
```javascript
const person = {
name: "Alice",
greet() {
console.log(this.name); // 'this' refers to person
}
};
```
```javascript
function outer() {
const message = "Hello";
function inner() {
console.log(message); // Can access 'message' from outer
}
inner();
}
```
┌─────────────────────────────────────────┐
│ EXECUTION CONTEXT │
│ Function: greet │
├─────────────────────────────────────────┤
│ Arguments: { name: "Alice" } │
│ Local Vars: { greeting: undefined } │
│ this: window (or undefined) │
│ Return to: line 12, main program │
│ Outer Scope: [global scope] │
└─────────────────────────────────────────┘
Let's look at a more complex example with multiple levels of function calls.
function multiply(x, y) {
return x * y;
}
function square(n) {
return multiply(n, n);
}
function printSquare(n) {
const result = square(n);
console.log(result);
}
printSquare(4);
**Step 2: printSquare calls square(4)**
```
Stack: [ square, printSquare ]
```
**Step 3: square calls multiply(4, 4)**
```
Stack: [ multiply, square, printSquare ]
```
This is the **maximum stack depth** for this program: 3 frames.
**Step 4: multiply returns 16**
```
Stack: [ square, printSquare ]
```
**Step 5: square returns 16**
```
Stack: [ printSquare ]
```
**Step 6: printSquare logs 16 and returns**
```
Stack: [ empty ]
```
**Output: `16`**
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────┐
│printSquare │ → │ square │ → │ multiply │ → │ square │ → │printSquare │ → │ (empty) │
└─────────────┘ ├─────────────┤ ├─────────────┤ ├─────────────┤ └─────────────┘ └─────────┘
│printSquare │ │ square │ │printSquare │
└─────────────┘ ├─────────────┤ └─────────────┘
│printSquare │
└─────────────┘
Depth: 1 Depth: 2 Depth: 3 Depth: 2 Depth: 1 Depth: 0
```
The call stack has a limited size. The default limit varies by engine — Chrome's V8 typically allows around 10,000–15,000 frames, while Firefox's SpiderMonkey has a similar threshold. If you keep adding functions without removing them, eventually you'll run out of space. This is called a stack overflow, and JavaScript throws a RangeError when it happens.
┌─────────────────────────────────────────────────────────────────────────┐
│ STACK OVERFLOW │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ WRONG: No Base Case RIGHT: With Base Case │
│ ──────────────────── ───────────────────── │
│ │
│ function count() { function count(n) { │
│ count() // Forever! if (n <= 0) return // Stop! │
│ } count(n - 1) │
│ } │
│ │
│ Stack grows forever... Stack grows, then shrinks │
│ ┌─────────┐ ┌─────────┐ │
│ │ count() │ │ count(0)│ ← Returns │
│ ├─────────┤ ├─────────┤ │
│ │ count() │ │ count(1)│ │
│ ├─────────┤ ├─────────┤ │
│ │ count() │ │ count(2)│ │
│ ├─────────┤ └─────────┘ │
│ │ .... │ │
│ └─────────┘ │
│ 💥 CRASH! ✓ Success! │
│ │
└─────────────────────────────────────────────────────────────────────────┘
// ❌ BAD: This will crash!
function countdown(n) {
console.log(n);
countdown(n - 1); // Calls itself forever!
}
countdown(5);
What happens:
Stack: [ countdown(5) ]
Stack: [ countdown(4), countdown(5) ]
Stack: [ countdown(3), countdown(4), countdown(5) ]
Stack: [ countdown(2), countdown(3), countdown(4), countdown(5) ]
... keeps growing forever ...
💥 CRASH: Maximum call stack size exceeded
// ✅ GOOD: This works correctly
function countdown(n) {
if (n <= 0) {
console.log("Done!");
return; // ← BASE CASE: Stop here!
}
console.log(n);
countdown(n - 1);
}
countdown(5);
// Output: 5, 4, 3, 2, 1, Done!
What happens now:
Stack: [ countdown(5) ]
Stack: [ countdown(4), countdown(5) ]
Stack: [ countdown(3), countdown(4), countdown(5) ]
Stack: [ countdown(2), countdown(3), ..., countdown(5) ]
Stack: [ countdown(1), countdown(2), ..., countdown(5) ]
Stack: [ countdown(0), countdown(1), ..., countdown(5) ]
↑ Base case reached! Start returning.
Stack: [ countdown(1), ..., countdown(5) ]
Stack: [ countdown(2), ..., countdown(5) ]
... stack unwinds ...
Stack: [ countdown(5) ]
Stack: [ empty ]
✅ Program completes successfully
| Browser | Error Message |
|---|---|
| Chrome | RangeError: Maximum call stack size exceeded |
| Firefox | InternalError: too much recursion (non-standard) |
| Safari | RangeError: Maximum call stack size exceeded |
const p = new Person();
p.name = "Alice"; // 💥 Crash!
// Fix: Use a different property name
class PersonFixed {
set name(value) {
this._name = value; // Use _name instead
}
}
```
a(); // 💥 Crash!
```
When something goes wrong, the call stack is your best friend for figuring out what happened.
When an error occurs, JavaScript gives you a stack trace, a snapshot of the call stack at the moment of the error.
function a() { b(); }
function b() { c(); }
function c() {
throw new Error('Something went wrong!');
}
a();
Output:
Error: Something went wrong!
at c (script.js:4:9)
at b (script.js:2:14)
at a (script.js:1:14)
at script.js:7:1
How to read it:
at c (script.js:4:9) = Error occurred in function c, file script.js, line 4, column 9function factorial(n) {
console.log('factorial called with n =', n);
if (n <= 1) return 1;
return n * factorial(n - 1);
}
You might be wondering: "If JavaScript can only do one thing at a time, how does it handle things like setTimeout or fetching data from a server?"
Great question! The call stack is only part of the picture.
<Note> When you use asynchronous functions like [`setTimeout`](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout), [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), or event listeners, JavaScript doesn't put them on the call stack immediately. Instead, they go through a different system involving the **[Event Loop](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Event_loop)** and **Task Queue**.This is covered in detail in the Event Loop section. </Note>
Here's a sneak peek:
console.log('First');
setTimeout(() => {
console.log('Second');
}, 0);
console.log('Third');
// Output:
// First
// Third
// Second ← Even with 0ms delay, this runs last!
The setTimeout callback doesn't go directly on the call stack. It waits in a queue until the stack is empty. As Philip Roberts demonstrated in his acclaimed JSConf EU talk "What the heck is the event loop?" (viewed over 8 million times), this is why "Third" prints before "Second" even though the timeout is 0 milliseconds.
| Component | Purpose | Structure |
|-----------|---------|-----------|
| **Call Stack** | Tracks function execution | Ordered (LIFO), small, fast |
| **Heap** | Stores data (objects, arrays) | Unstructured, large |
```javascript
function example() {
// Primitives live in the stack frame
const x = 10;
const name = "Alice";
// Objects live in the HEAP (reference stored in stack)
const user = { name: "Alice" };
const numbers = [1, 2, 3];
}
```
When the function returns, the stack frame is popped (primitives gone), but heap objects persist until garbage collected.
1. The call stack to be completely empty
2. All microtasks to be processed first
3. Its turn in the task queue
```javascript
console.log('Start');
setTimeout(() => {
console.log('Timer'); // Does NOT run at 0ms!
}, 0);
console.log('End');
// Output: Start, End, Timer
// Even with 0ms delay, 'Timer' prints LAST
```
The callback must wait until the current script finishes and the stack is empty.
```javascript
function a() {
console.log('A start');
b(); // JS pauses 'a' and runs 'b' completely
console.log('A end');
}
function b() {
console.log('B');
}
a();
// Output: A start, B, A end (sequential, not parallel)
```
**The source of confusion:** People mistake JavaScript's *asynchronous behavior* for *parallel execution*. Web APIs (timers, fetch, etc.) run in separate browser threads, but JavaScript code itself runs one operation at a time. The Event Loop coordinates callbacks, creating the *illusion* of concurrency.
```javascript
console.log('1');
new Promise((resolve) => {
console.log('2'); // Runs SYNCHRONOUSLY!
resolve();
}).then(() => {
console.log('3'); // Async (microtask)
});
console.log('4');
// Output: 1, 2, 4, 3
// Note: '2' prints before '4'!
```
The executor function passed to `new Promise()` runs immediately on the call stack. Only the `.then()`, `.catch()`, and `.finally()` callbacks are queued as microtasks.
JavaScript is single-threaded — It has ONE call stack and can only do one thing at a time
LIFO principle — Last In, First Out. The most recent function call finishes first
Execution contexts — Each function call creates a "frame" containing arguments, local variables, and return address
Synchronous execution — Functions must complete before their callers can continue
Stack overflow — Happens when the stack gets too deep, usually from infinite recursion
Always have a base case — Recursive functions need a stopping condition
Stack traces are your friend — They show you exactly how your program got to an error
Async callbacks wait — setTimeout, fetch, and event callbacks don't run until the call stack is empty
Each frame is isolated — Local variables in one function call don't affect variables in another call of the same function
Debugging tools show the stack — Browser DevTools let you pause execution and inspect the current call stack
</Info>It's important because it determines the order in which functions execute and return. The most recently called function must complete before the function that called it can continue. This is how JavaScript keeps track of nested function calls and knows where to return when a function finishes.
**Answer:** The maximum stack depth is **4 frames**.
```
Stack at deepest point: [ d, c, b, a ]
```
When `d()` is executing, all four functions are on the stack. After `d()` logs "done" and returns, the stack starts unwinding.
**Answer:** This code causes a stack overflow because there's **no base case** to stop the recursion.
- `greet()` is called
- `greet()` calls `greet()` again
- That `greet()` calls `greet()` again
- This continues forever, adding new frames to the stack
- Eventually the stack runs out of space → **Maximum call stack size exceeded**
**Fix:** Add a condition to stop the recursion:
```javascript
function greet(times) {
if (times <= 0) return; // Base case
console.log('Hello!');
greet(times - 1);
}
greet(3);
```
1. **Function arguments** — The values passed to the function
2. **Local variables** — Variables declared with `var`, `let`, or `const`
3. **The `this` value** — The context binding for the function
4. **Return address** — Where to continue executing after the function returns
5. **Scope chain** — Access to variables from outer (parent) functions
This is why each function call can have its own independent set of variables without interfering with other calls.
setTimeout(() => {
console.log('Second')
}, 0)
console.log('Third')
```
**Answer:** The output is:
```
First
Third
Second
```
Even though `setTimeout` has a 0ms delay, "Second" prints last because:
1. `setTimeout` doesn't put the callback directly on the call stack
2. Instead, the callback waits in the **task queue**
3. The event loop only moves it to the call stack when the stack is empty
4. "Third" runs first because it's already on the call stack
This demonstrates that the call stack must be empty before async callbacks execute.
**Answer:** Read stack traces from **top to bottom** (most recent to oldest):
1. **Top line** (`at c`) — Where the error actually occurred (function `c`, line 4, column 9)
2. **Following lines** — The chain of function calls that led here
3. **Bottom line** — Where the chain started (the initial call)
The trace tells you: the program started at line 7, called `a()`, which called `b()`, which called `c()`, where the error was thrown. This helps you trace back through your code to find the root cause.