From Zero to AI

Lesson 1.2: Call Stack and Event Loop

Duration: 60 minutes

Learning Objectives

By the end of this lesson, you will be able to:

  • Understand what the call stack is and how it works
  • Explain what happens when functions call other functions
  • Describe Web APIs and how they handle async operations
  • Understand the callback queue and its role
  • Explain how the event loop coordinates everything

The Call Stack

The call stack is a data structure that keeps track of where the program is in its execution. Think of it as a stack of plates - you add plates to the top and remove them from the top (Last In, First Out - LIFO).

function greet(): void {
  console.log('Hello!');
}

greet();

How the Stack Works

When you call a function, it gets pushed onto the stack. When the function returns, it gets popped off.

Step 1: Program starts
Stack: [main()]

Step 2: greet() is called
Stack: [main(), greet()]

Step 3: console.log() is called
Stack: [main(), greet(), console.log()]

Step 4: console.log() completes and pops off
Stack: [main(), greet()]

Step 5: greet() completes and pops off
Stack: [main()]

Step 6: Program ends
Stack: []

Visualizing the Call Stack

Let us trace through a more complex example:

function multiply(a: number, b: number): number {
  return a * b;
}

function square(n: number): number {
  return multiply(n, n);
}

function printSquare(n: number): void {
  const result = square(n);
  console.log(result);
}

printSquare(4);

Stack Trace

1. printSquare(4) pushed
   Stack: [main, printSquare]

2. square(4) pushed
   Stack: [main, printSquare, square]

3. multiply(4, 4) pushed
   Stack: [main, printSquare, square, multiply]

4. multiply returns 16, popped
   Stack: [main, printSquare, square]

5. square returns 16, popped
   Stack: [main, printSquare]

6. console.log(16) pushed
   Stack: [main, printSquare, console.log]

7. console.log completes, popped
   Stack: [main, printSquare]

8. printSquare completes, popped
   Stack: [main]

9. main completes
   Stack: []

Stack Overflow

What happens if the stack gets too deep?

function infinite(): void {
  infinite(); // Calls itself forever
}

infinite();
// Error: Maximum call stack size exceeded

This is called a stack overflow - the stack runs out of space because functions keep being added without being removed.

// A common mistake: forgetting the base case in recursion
function countdown(n: number): void {
  console.log(n);
  countdown(n - 1); // Never stops!
}

// Fixed version:
function countdownFixed(n: number): void {
  if (n < 0) return; // Base case - stops recursion
  console.log(n);
  countdownFixed(n - 1);
}

The Problem: Blocking Operations

Remember, JavaScript is single-threaded. If something takes a long time, it blocks everything:

// Imagine this function takes 5 seconds
function slowOperation(): void {
  // Heavy computation...
  const start = Date.now();
  while (Date.now() - start < 5000) {
    // Blocking for 5 seconds
  }
  console.log('Done!');
}

console.log('Start');
slowOperation(); // Everything freezes for 5 seconds!
console.log('End');

While slowOperation runs:

  • The user cannot click buttons
  • Animations freeze
  • The page becomes unresponsive

This is terrible for user experience! We need a way to handle slow operations without blocking.


Web APIs / Node APIs

The solution is to delegate slow tasks to the runtime environment. The browser or Node.js provides APIs that can handle operations outside the main JavaScript thread.

console.log('Start');

// setTimeout is NOT JavaScript - it's a Web API
setTimeout(() => {
  console.log('Timer done!');
}, 2000);

console.log('End');

Output:

Start
End
Timer done!  // After 2 seconds

Wait - "End" printed before "Timer done!" even though it comes after in the code! This is where the event loop comes in.

Common Web APIs / Node APIs

API Purpose Example
setTimeout Delay execution setTimeout(fn, 1000)
setInterval Repeat execution setInterval(fn, 1000)
fetch HTTP requests fetch(url)
addEventListener Handle events element.addEventListener('click', fn)
fs.readFile (Node) Read files fs.readFile(path, callback)

The Callback Queue

When a Web API finishes its work, it does not immediately run its callback. Instead, the callback goes to the callback queue (also called the task queue).

console.log('First');

setTimeout(() => {
  console.log('Timeout callback');
}, 0); // Even with 0ms delay!

console.log('Second');

Output:

First
Second
Timeout callback

Even with a 0ms delay, "Timeout callback" prints last. The callback must wait in the queue until the call stack is empty.


The Event Loop

The event loop is a simple process that constantly checks:

  1. Is the call stack empty?
  2. Is there anything in the callback queue?
  3. If yes to both, move the first callback from the queue to the stack

The Loop in Action

console.log('1');

setTimeout(() => {
  console.log('2');
}, 0);

console.log('3');

Step by step:

1. console.log("1") → Stack: [log] → prints "1" → Stack: []

2. setTimeout called → Timer registered with Web API
   Web API: [timer(0ms, callback)]

3. console.log("3") → Stack: [log] → prints "3" → Stack: []

4. Timer completes → callback goes to queue
   Queue: [callback]

5. Event loop: Stack empty? Yes. Queue has item? Yes.
   Move callback to stack
   Stack: [callback]

6. callback executes → prints "2" → Stack: []

Output: 1, 3, 2


Multiple Callbacks

Let us trace a more complex example:

console.log('Start');

setTimeout(() => {
  console.log('Timeout 1');
}, 1000);

setTimeout(() => {
  console.log('Timeout 2');
}, 500);

setTimeout(() => {
  console.log('Timeout 3');
}, 0);

console.log('End');

Execution Order

  1. "Start" prints (synchronous)
  2. Three timers are registered with Web API
  3. "End" prints (synchronous)
  4. Stack is now empty
  5. After 0ms: "Timeout 3" callback enters queue, executes
  6. After 500ms: "Timeout 2" callback enters queue, executes
  7. After 1000ms: "Timeout 1" callback enters queue, executes

Output:

Start
End
Timeout 3
Timeout 2
Timeout 1

A Real-World Analogy

Think of a restaurant:

  • Call Stack = The chef (can only cook one dish at a time)
  • Web APIs = Kitchen assistants (can prep ingredients while chef cooks)
  • Callback Queue = Ready dishes waiting to be plated
  • Event Loop = The chef checking if there are ready dishes to plate
Customer orders → Chef cooks → Assistant preps next order
                             → Prepped order waits in queue
                             → Chef finishes, picks up next from queue

The chef (main thread) never stops, but assistants (Web APIs) help by doing preparation work asynchronously.


The Microtask Queue

There is actually another queue with higher priority: the microtask queue. Promises use this queue.

console.log('1');

setTimeout(() => {
  console.log('2');
}, 0);

Promise.resolve().then(() => {
  console.log('3');
});

console.log('4');

Output:

1
4
3
2

Microtasks (Promises) run before regular callbacks (setTimeout). We will explore this more when we study Promises.

Queue Priority


Exercises

Exercise 1: Predict the Output

What will this code print?

console.log('A');

setTimeout(() => {
  console.log('B');
}, 0);

console.log('C');

setTimeout(() => {
  console.log('D');
}, 0);

console.log('E');
Solution
A
C
E
B
D

Explanation:

  1. "A" prints (sync)
  2. First setTimeout - callback goes to Web API
  3. "C" prints (sync)
  4. Second setTimeout - callback goes to Web API
  5. "E" prints (sync)
  6. Stack empty, callbacks execute in order: "B", "D"

Exercise 2: With Nested Callbacks

What is the output?

console.log('1');

setTimeout(() => {
  console.log('2');
  setTimeout(() => {
    console.log('3');
  }, 0);
  console.log('4');
}, 0);

console.log('5');
Solution
1
5
2
4
3

Explanation:

  1. "1" prints
  2. First timeout registered
  3. "5" prints
  4. First callback runs: "2", nested timeout registered, "4"
  5. Nested callback runs: "3"

Exercise 3: Trace the Stack

Draw the call stack state for each step:

function a(): void {
  console.log('a');
}

function b(): void {
  console.log('b');
  a();
}

b();
console.log('done');
Solution
Step 1: b() called
Stack: [main, b]

Step 2: console.log("b")
Stack: [main, b, log]
Output: "b"

Step 3: log completes
Stack: [main, b]

Step 4: a() called
Stack: [main, b, a]

Step 5: console.log("a")
Stack: [main, b, a, log]
Output: "a"

Step 6: log completes
Stack: [main, b, a]

Step 7: a() completes
Stack: [main, b]

Step 8: b() completes
Stack: [main]

Step 9: console.log("done")
Stack: [main, log]
Output: "done"

Step 10: log completes
Stack: [main]

Final output: b, a, done

Exercise 4: Fix the Bug

This code should print numbers 1-5 with a delay between each, but it prints "6" five times. Why? Fix it.

for (var i = 1; i <= 5; i++) {
  setTimeout(() => {
    console.log(i);
  }, i * 1000);
}
Solution

The problem: var is function-scoped, so by the time the callbacks run, i is already 6.

Fix using let (block-scoped):

for (let i = 1; i <= 5; i++) {
  setTimeout(() => {
    console.log(i);
  }, i * 1000);
}

Each iteration creates a new i in its own scope.


Key Takeaways

  1. Call Stack tracks function execution (LIFO - Last In, First Out)
  2. Stack Overflow occurs when too many functions are stacked
  3. Web APIs handle async operations outside the main thread
  4. Callback Queue holds callbacks waiting to be executed
  5. Event Loop moves callbacks to the stack when it is empty
  6. Microtask Queue (Promises) has higher priority than callback queue

Resources

Resource Type Description
Loupe - Event Loop Visualizer Tool Interactive visualization
MDN: Concurrency Model Documentation Official event loop docs
What the heck is the event loop anyway? Video Philip Roberts' famous talk

Next Lesson

Now that you understand the mechanics, let us explore the practical differences between synchronous and asynchronous code.

Continue to Lesson 1.3: Synchronous vs Asynchronous Code