Lesson 1.4: Visualizing the Event Loop
Duration: 70 minutes
Learning Objectives
By the end of this lesson, you will be able to:
- Use visualization tools to understand async code execution
- Trace complex async code step by step
- Predict execution order in challenging scenarios
- Debug timing-related issues in JavaScript
- Understand the difference between macrotasks and microtasks
Visualization Tools
The best way to understand the event loop is to see it in action. Here are some excellent tools:
Loupe - Event Loop Visualizer
Loupe by Philip Roberts is an interactive tool that shows:
- The call stack
- Web APIs
- The callback queue
- The event loop in action
Try pasting this code into Loupe:
console.log('Start');
setTimeout(function timer() {
console.log('Timer');
}, 1000);
console.log('End');
Watch how:
- "Start" and "End" go straight through the call stack
- The timer callback moves to Web APIs
- After 1 second, it enters the callback queue
- The event loop moves it to the call stack when empty
JavaScript Visualizer 9000
JS Visualizer 9000 shows:
- Execution context
- Task queue
- Microtask queue
- Visual stepping through code
Step-by-Step Tracing
Let us trace this code manually:
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');
The Trace
Final output: 1, 4, 3, 2
Macrotasks vs Microtasks
JavaScript has two types of async queues:
Macrotasks (Task Queue)
setTimeoutsetIntervalsetImmediate(Node.js)- I/O operations
- UI rendering
Microtasks (Microtask Queue)
Promise.then/catch/finallyqueueMicrotaskMutationObserverprocess.nextTick(Node.js)
Priority Order
The event loop processes ALL microtasks before moving to the next macrotask.
Example: Microtasks vs Macrotasks
console.log('Script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve()
.then(() => {
console.log('Promise 1');
})
.then(() => {
console.log('Promise 2');
});
console.log('Script end');
Output:
Script start
Script end
Promise 1
Promise 2
setTimeout
Both Promise callbacks run before setTimeout, even though setTimeout was registered first!
Complex Examples
Example 1: Nested Promises and Timers
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
});
}, 0);
Promise.resolve().then(() => {
console.log('4');
setTimeout(() => {
console.log('5');
}, 0);
});
console.log('6');
What is the output?
1
6
4
2
3
5
Explanation:
- "1" - sync
- setTimeout registered (macrotask)
- Promise.then registered (microtask)
- "6" - sync
- Stack empty, process microtasks: "4", new setTimeout registered
- Process macrotask (first setTimeout): "2", new microtask added
- Process microtasks: "3"
- Process macrotask (second setTimeout): "5"
Example 2: Multiple Microtask Sources
console.log('Start');
queueMicrotask(() => {
console.log('Microtask 1');
});
Promise.resolve().then(() => {
console.log('Promise 1');
});
queueMicrotask(() => {
console.log('Microtask 2');
});
Promise.resolve().then(() => {
console.log('Promise 2');
});
console.log('End');
What is the output?
Start
End
Microtask 1
Promise 1
Microtask 2
Promise 2
Microtasks are processed in the order they were added to the queue.
Example 3: The Tricky One
async function async1(): Promise<void> {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2(): Promise<void> {
console.log('async2');
}
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
async1();
new Promise<void>((resolve) => {
console.log('promise1');
resolve();
}).then(() => {
console.log('promise2');
});
console.log('script end');
What is the output?
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
Explanation:
- "script start" - sync
- setTimeout registered
- async1() called: "async1 start", async2() called: "async2"
- await pauses async1, rest becomes microtask
- Promise executor runs: "promise1", .then becomes microtask
- "script end" - sync
- Microtasks: "async1 end", "promise2"
- Macrotask: "setTimeout"
Debugging Async Code
Using console.log with Timestamps
function log(message: string): void {
const time = new Date().toISOString().substr(11, 12);
console.log(`[${time}] ${message}`);
}
log('Start');
setTimeout(() => log('Timeout 1'), 1000);
setTimeout(() => log('Timeout 2'), 500);
Promise.resolve().then(() => log('Promise'));
log('End');
Output:
[12:00:00.000] Start
[12:00:00.001] End
[12:00:00.001] Promise
[12:00:00.502] Timeout 2
[12:00:01.001] Timeout 1
Using Labels
console.log('--- Sync Phase ---');
console.log('A');
setTimeout(() => {
console.log('--- Macrotask 1 ---');
console.log('B');
}, 0);
Promise.resolve().then(() => {
console.log('--- Microtask 1 ---');
console.log('C');
});
console.log('D');
console.log('--- End Sync Phase ---');
Common Pitfalls
Pitfall 1: Assuming setTimeout(fn, 0) Runs Immediately
let value = 'initial';
setTimeout(() => {
value = 'changed';
}, 0);
console.log(value); // "initial" - not "changed"!
Even with 0ms delay, the callback waits for the call stack to clear.
Pitfall 2: Forgetting Microtask Priority
let result = '';
setTimeout(() => {
result += 'A';
}, 0);
Promise.resolve().then(() => {
result += 'B';
});
// Later...
setTimeout(() => {
console.log(result); // "BA" - not "AB"!
}, 10);
Pitfall 3: Blocking the Event Loop
setTimeout(() => {
console.log('This should print after 100ms');
}, 100);
// This blocks for 2 seconds!
const start = Date.now();
while (Date.now() - start < 2000) {
// Blocking loop
}
// The timeout callback runs after 2 seconds, not 100ms!
The event loop cannot process callbacks while synchronous code is running.
Building a Mental Model
Think of the event loop like this:
Exercises
Exercise 1: Trace This Code
Draw the state of the call stack, microtask queue, and macrotask queue at each step:
console.log('A');
Promise.resolve().then(() => {
console.log('B');
});
setTimeout(() => {
console.log('C');
}, 0);
console.log('D');
Solution
Step 1: console.log("A")
Stack: [main, log] → Output: A
Microtasks: []
Macrotasks: []
Step 2: Promise.then registered
Stack: [main]
Microtasks: [B-callback]
Macrotasks: []
Step 3: setTimeout registered
Stack: [main]
Microtasks: [B-callback]
Macrotasks: [C-callback]
Step 4: console.log("D")
Stack: [main, log] → Output: D
Microtasks: [B-callback]
Macrotasks: [C-callback]
Step 5: main completes, process microtasks
Stack: [B-callback] → Output: B
Microtasks: []
Macrotasks: [C-callback]
Step 6: process macrotasks
Stack: [C-callback] → Output: C
Microtasks: []
Macrotasks: []
Final output: A, D, B, C
Exercise 2: Fix the Bug
This code should print "Processing complete!" after all items are processed. Fix it:
const items = [1, 2, 3];
items.forEach((item) => {
setTimeout(() => {
console.log('Processed:', item);
}, 100);
});
console.log('Processing complete!');
Solution
const items = [1, 2, 3];
let processed = 0;
items.forEach((item) => {
setTimeout(() => {
console.log('Processed:', item);
processed++;
if (processed === items.length) {
console.log('Processing complete!');
}
}, 100);
});
Or using Promises (preferred):
const items = [1, 2, 3];
const promises = items.map((item) => {
return new Promise<void>((resolve) => {
setTimeout(() => {
console.log('Processed:', item);
resolve();
}, 100);
});
});
Promise.all(promises).then(() => {
console.log('Processing complete!');
});
Exercise 3: Predict the Order
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve()
.then(() => console.log('3'))
.then(() => console.log('4'));
Promise.resolve().then(() => {
console.log('5');
setTimeout(() => console.log('6'), 0);
});
console.log('7');
Solution
1
7
3
5
4
2
6
Explanation:
- "1" - sync
- setTimeout(2) registered as macrotask
- Promise chain (3, 4) registered as microtasks
- Promise (5, setTimeout(6)) registered as microtask
- "7" - sync
- Process microtasks in order:
- "3" prints, "4" queued
- "5" prints, setTimeout(6) registered
- "4" prints
- Process macrotasks:
- "2" prints
- "6" prints
Exercise 4: Real-World Scenario
You are loading a user profile. Predict the console output:
console.log('Loading profile...');
// Simulate API call
const fetchUser = new Promise<string>((resolve) => {
console.log('Fetching user data...');
setTimeout(() => {
resolve('User: Alice');
}, 100);
});
fetchUser
.then((user) => {
console.log(user);
console.log('Fetching posts...');
return new Promise<string>((resolve) => {
setTimeout(() => {
resolve('Posts loaded');
}, 50);
});
})
.then((posts) => {
console.log(posts);
});
setTimeout(() => {
console.log('Timeout 1');
}, 100);
console.log('Profile component rendered');
Solution
Loading profile...
Fetching user data...
Profile component rendered
Timeout 1 (after ~100ms - may race with next line)
User: Alice (after ~100ms)
Fetching posts...
Posts loaded (after ~150ms total)
Note: "Timeout 1" and "User: Alice" both trigger around 100ms, so their order may vary depending on exact timing. In most JavaScript engines, they will appear in this order since they were registered at the same time and setTimeout callbacks are processed in registration order.
Key Takeaways
- Use visualization tools like Loupe and JS Visualizer to understand async code
- Microtasks (Promises) have higher priority than macrotasks (setTimeout)
- All microtasks run before the next macrotask
- The event loop constantly checks if the stack is empty to process queued callbacks
- setTimeout(fn, 0) does not run immediately - it waits for the stack to clear
- Blocking code prevents the event loop from processing callbacks
- Trace code step by step to understand complex async behavior
Resources
| Resource | Type | Description |
|---|---|---|
| Loupe | Tool | Interactive event loop visualizer |
| JS Visualizer 9000 | Tool | Detailed async visualization |
| Jake Archibald: In The Loop | Video | Deep dive into the event loop |
| MDN: Microtask Guide | Documentation | Official microtask documentation |
Module Summary
Congratulations! You have completed Module 1. You now understand:
- How JavaScript engines execute code
- The single-threaded nature of JavaScript
- How the call stack tracks function execution
- The role of Web APIs in handling async operations
- The difference between callback queue and microtask queue
- How the event loop coordinates everything
This foundation is essential for understanding Promises and async/await, which we will cover in the next modules.
Next Module
You are now ready to learn about Promises - a better way to handle asynchronous code that avoids callback hell.