How Call Stack Works
Part 1: Step-by-Step Trace
The best way to understand the call stack is to trace through code manually. Let's watch each push and pop happen.
Example 1 — Two function calls in sequence
javascript
function multiply(a, b) {
return a * b;
}
function square(n) {
return multiply(n, n);
}
let result = square(4);
console.log(result); // 16
// Step-by-step call stack:
//
// → square(4) called
// Stack: [ square ]
//
// → multiply(4, 4) called inside square
// Stack: [ square | multiply ] ← multiply is on top
//
// → multiply returns 16
// Stack: [ square ] ← multiply popped
//
// → square returns 16
// Stack: [] ← square popped
//
// → console.log(16) called
// Stack: [ console.log ] → runs → popped
// Stack: []Example 2 — Function with multiple calls
javascript
function printName() {
let first = getFirst();
let last = getLast();
console.log(first + " " + last);
}
function getFirst() { return "Arjun"; }
function getLast() { return "Kumar"; }
printName();
// Stack trace:
// printName pushed
// → getFirst pushed → returns "Arjun" → getFirst popped
// → getLast pushed → returns "Kumar" → getLast popped
// → console.log pushed → logs "Arjun Kumar" → popped
// printName popped
// Stack: []Part 2: The Call Stack and Recursion
Recursion stacks up frames quickly because each recursive call adds a new frame before any of them have returned. The stack only unwinds after hitting the base case.
Factorial — recursive stack build-up
javascript
function factorial(n) {
if (n <= 1) return 1; // base case
return n * factorial(n - 1);
}
factorial(4);
// Stack builds up:
// [ factorial(4) ]
// [ factorial(4) | factorial(3) ]
// [ factorial(4) | factorial(3) | factorial(2) ]
// [ factorial(4) | factorial(3) | factorial(2) | factorial(1) ]
// ↑ hits base case!
// Stack unwinds:
// factorial(1) returns 1 → popped
// factorial(2) returns 2*1 → popped
// factorial(3) returns 3*2 → popped
// factorial(4) returns 4*6 → popped → result: 24Part 3: Synchronous — Stack Must Be Empty for Async
This is the most important thing to understand: async callbacks only run when the call stack is completely empty.The event loop will not push a callback onto the stack while something else is running.
setTimeout with 0ms — still async
javascript
function first() {
console.log("1");
setTimeout(() => console.log("2"), 0); // 0ms delay!
console.log("3");
}
first();
// Output: 1 3 2
//
// Why? setTimeout sends callback to Web API even with 0ms.
// The callback goes to the queue.
// The event loop only picks it up AFTER the stack is empty.
//
// Stack trace:
// first() pushed
// → logs "1"
// → setTimeout registers callback (Web API)
// → logs "3"
// first() popped → Stack is empty!
// Event loop: picks up setTimeout callback
// → logs "2"Long-running sync code blocks everything
javascript
// ❌ This blocks the entire browser for ~3 seconds
function blockFor3Seconds() {
const start = Date.now();
while (Date.now() - start < 3000) {} // busy-waiting
console.log("Done blocking");
}
setTimeout(() => console.log("Timer fired"), 500);
blockFor3Seconds();
// Output:
// Done blocking (after 3s)
// Timer fired (after 3s, even though it was set for 0.5s!)
//
// The timer callback was stuck in the queue
// because the stack was never empty until blockFor3Seconds returned.