12.2-Call-Stack
6.2 The Call Stack
Introduction
The call stack is how JavaScript keeps track of function calls. It's a LIFO (Last In, First Out) data structure that manages execution contexts. Understanding the call stack is essential for debugging and understanding how your code executes.
What is the Call Stack?
The call stack is a mechanism for the JavaScript engine to keep track of:
- β’Which function is currently executing
- β’Which functions are waiting to resume
- β’Where to return after a function completes
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CALL STACK β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β βββββββββββββββββββββββ β
β β third() β β Currently executing β
β βββββββββββββββββββββββ€ β
β β second() β β Waiting β
β βββββββββββββββββββββββ€ β
β β first() β β Waiting β
β βββββββββββββββββββββββ€ β
β β Global β β Base β
β βββββββββββββββββββββββ β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
LIFO: Last In, First Out
PUSH (add to top) POP (remove from top)
β β
βββββββββββββββ βββββββββββββββ
β third() β β third() β β removed
βββββββββββββββ€ βββββββββββββββ€
β second() β β second() β
βββββββββββββββ€ βββββββββββββββ€
β first() β β first() β
βββββββββββββββ βββββββββββββββ
- β’Push: When a function is called, its context is pushed onto the stack
- β’Pop: When a function returns, its context is popped off the stack
Call Stack in Action
function first() {
console.log('First function');
second();
console.log('First function done');
}
function second() {
console.log('Second function');
third();
console.log('Second function done');
}
function third() {
console.log('Third function');
}
first();
Step-by-Step Execution:
Step 1: Program starts
βββββββββββββββββββ
β Global β
βββββββββββββββββββ
Step 2: first() called
βββββββββββββββββββ
β first() β
βββββββββββββββββββ€
β Global β
βββββββββββββββββββ
Output: "First function"
Step 3: second() called from first()
βββββββββββββββββββ
β second() β
βββββββββββββββββββ€
β first() β
βββββββββββββββββββ€
β Global β
βββββββββββββββββββ
Output: "Second function"
Step 4: third() called from second()
βββββββββββββββββββ
β third() β
βββββββββββββββββββ€
β second() β
βββββββββββββββββββ€
β first() β
βββββββββββββββββββ€
β Global β
βββββββββββββββββββ
Output: "Third function"
Step 5: third() returns (popped)
βββββββββββββββββββ
β second() β
βββββββββββββββββββ€
β first() β
βββββββββββββββββββ€
β Global β
βββββββββββββββββββ
Output: "Second function done"
Step 6: second() returns (popped)
βββββββββββββββββββ
β first() β
βββββββββββββββββββ€
β Global β
βββββββββββββββββββ
Output: "First function done"
Step 7: first() returns (popped)
βββββββββββββββββββ
β Global β
βββββββββββββββββββ
Program continues...
Stack Frames
Each entry in the call stack is called a stack frame. A stack frame contains:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β STACK FRAME β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β β’ Function being executed β
β β’ Local variables β
β β’ Arguments passed β
β β’ Return address (where to continue after return) β
β β’ 'this' binding β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Stack Overflow
The call stack has a limited size. If too many functions are pushed without popping, you get a stack overflow:
// DANGER: This causes stack overflow!
function recursive() {
recursive(); // No base case!
}
recursive();
// RangeError: Maximum call stack size exceeded
Visual of Stack Overflow:
βββββββββββββββββββ
β recursive() β
βββββββββββββββββββ€
β recursive() β
βββββββββββββββββββ€
β recursive() β
βββββββββββββββββββ€
β ... β Stack keeps growing...
βββββββββββββββββββ€
β recursive() β
βββββββββββββββββββ€
β Global β
βββββββββββββββββββ
β
π₯ OVERFLOW!
Proper Recursion (with base case):
function countdown(n) {
if (n <= 0) {
console.log('Done!');
return; // Base case - stops recursion
}
console.log(n);
countdown(n - 1);
}
countdown(3);
// 3, 2, 1, Done!
Debugging with the Call Stack
Browser DevTools
When you hit a breakpoint or error, you can see the call stack:
function calculateTax(amount) {
debugger; // Breakpoint
return amount * 0.1;
}
function getTotal(price) {
const tax = calculateTax(price);
return price + tax;
}
function checkout(items) {
const price = items.reduce((sum, i) => sum + i, 0);
return getTotal(price);
}
checkout([10, 20, 30]);
DevTools Call Stack Panel:
βΌ calculateTax (app.js:2)
getTotal (app.js:7)
checkout (app.js:12)
(anonymous) (app.js:15)
Stack Traces in Errors
function a() {
b();
}
function b() {
c();
}
function c() {
throw new Error('Something went wrong!');
}
a();
Error Output:
Error: Something went wrong!
at c (script.js:11)
at b (script.js:7)
at a (script.js:3)
at script.js:14
Read from top to bottom:
- β’Error occurred in
c() - β’
c()was called byb() - β’
b()was called bya() - β’
a()was called from global scope (line 14)
Call Stack and Asynchronous Code
The call stack only handles synchronous code. Async operations use the event loop:
console.log('1. Start');
setTimeout(() => {
console.log('3. Timeout callback');
}, 0);
console.log('2. End');
// Output:
// 1. Start
// 2. End
// 3. Timeout callback
Why?
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 1. Call stack runs synchronous code β
β - "1. Start" logged β
β - setTimeout schedules callback (to task queue) β
β - "2. End" logged β
β - Call stack empty β
β β
β 2. Event loop checks: Is call stack empty? β
β - Yes! Move callback from queue to stack β
β β
β 3. Callback executes β
β - "3. Timeout callback" logged β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Single-Threaded Execution
JavaScript has one call stack = single-threaded:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β JAVASCRIPT RUNTIME β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β βββββββββββββββ βββββββββββββββββββββββββββββββ β
β β Call Stack β β Web APIs β β
β β β β (setTimeout, fetch, DOM) β β
β β [one only] β β β β
β βββββββββββββββ βββββββββββββββββββββββββββββββ β
β β β β
β β β β
β ββββββββ΄βββββββββββββββββββββββββββββββββββββββββ β
β β Callback Queue β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β β β β EVENT LOOP (moves callbacks to stack) β β β β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Blocking the Call Stack
Long-running code blocks everything:
// DON'T DO THIS!
function blockFor(ms) {
const start = Date.now();
while (Date.now() - start < ms) {
// Busy waiting - blocks the stack!
}
}
console.log('Before');
blockFor(3000); // UI frozen for 3 seconds!
console.log('After');
Better: Use async patterns
console.log('Before');
setTimeout(() => {
console.log('After 3 seconds');
}, 3000);
console.log('Continuing...');
// UI stays responsive
Maximum Stack Size
Different browsers have different limits:
| Browser | Approximate Limit |
|---|---|
| Chrome | ~10,000 - 15,000 |
| Firefox | ~50,000 |
| Safari | ~50,000 |
| Node.js | ~10,000 - 20,000 |
// Testing stack size (don't run in production!)
function countStack(n = 0) {
try {
return countStack(n + 1);
} catch (e) {
return n;
}
}
console.log('Max stack size:', countStack());
Tail Call Optimization (ES6)
In theory, ES6 supports tail call optimization (TCO), where recursive calls in "tail position" don't add to the stack:
// Tail recursive (optimizable in theory)
function factorialTail(n, accumulator = 1) {
if (n <= 1) return accumulator;
return factorialTail(n - 1, n * accumulator); // Tail call
}
// NOT tail recursive
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1); // Has pending multiplication
}
Note: Only Safari implements TCO. Other engines don't, so avoid deep recursion.
Summary
| Concept | Description |
|---|---|
| Call Stack | Tracks function calls (LIFO) |
| Stack Frame | One entry per function call |
| Push | Add function to stack (on call) |
| Pop | Remove function from stack (on return) |
| Stack Overflow | Too many calls without returns |
| Stack Trace | Shows path of function calls |
| Single-Threaded | Only one call stack |
| Blocking | Long sync code freezes everything |
Debugging Tips
- β’Use
console.trace()- Prints current call stack - β’Use DevTools breakpoints - See stack in real-time
- β’Read stack traces bottom-up - Find the origin of calls
- β’Watch for deep recursion - Potential stack overflow
function debugMe() {
console.trace('Current call stack:');
}
function caller() {
debugMe();
}
caller();
// Prints the call stack at that moment
Next Steps
In the next section, we'll take a deep dive into hoistingβunderstanding exactly how variables and functions are set up during the creation phase.