Learning Objectives
- Understand the components of the Event Loop
- Learn how the call stack, task queue, and microtask queue work
- Master the execution order of async code
- Predict how setTimeout, Promises, and async/await execute
What is the Event Loop?
JavaScript is single-threaded, meaning it can only execute one piece of code at a time. Yet it handles asynchronous operations like network requests, timers, and user interactions without blocking. How? The Event Loop.
The Event Loop is a mechanism that coordinates the execution of code, handling events, and executing queued tasks. It's what makes JavaScript's non-blocking asynchronous behavior possible.
The Components
1. The Call Stack
The call stack is where JavaScript keeps track of function execution. When a function is called, it's pushed onto the stack. When it returns, it's popped off.
function first() {
console.log('First');
}
function second() {
first();
console.log('Second');
}
second();
// Call stack progression:
// 1. second() pushed
// 2. first() pushed
// 3. first() pops (logs "First")
// 4. second() pops (logs "Second")
2. Web APIs / Browser APIs
When you call setTimeout, make a fetch request, or add an event listener, these operations are handled by the browser's Web APIs, not JavaScript itself. This is how JavaScript can be non-blocking.
console.log('Start');
setTimeout(() => {
console.log('Timeout');
}, 0);
console.log('End');
// Output:
// Start
// End
// Timeout (even though delay is 0!)
setTimeout is handed off to the Web API, and its callback goes to the task queue, which only executes after the call stack is empty.
3. The Task Queue (Macrotask Queue)
When Web APIs complete (like a timer finishing or a network request returning), their callbacks are placed in the task queue. The Event Loop checks if the call stack is empty, then moves tasks from the queue to the stack.
Macrotasks include:
setTimeoutsetIntervalsetImmediate(Node.js)- I/O operations
- UI rendering
4. The Microtask Queue
Microtasks have higher priority than macrotasks. After each macrotask, the Event Loop processes all microtasks before moving to the next macrotask.
Microtasks include:
- Promise callbacks (
.then,.catch,.finally) queueMicrotask()MutationObserverprocess.nextTick()(Node.js - even higher priority)
The Event Loop in Action
console.log('1: Sync');
setTimeout(() => {
console.log('2: setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('3: Promise');
});
console.log('4: Sync');
// Output:
// 1: Sync
// 4: Sync
// 3: Promise
// 2: setTimeout
- Synchronous code executes first: "1: Sync", "4: Sync"
- Call stack is empty, check microtask queue
- Promise callback executes: "3: Promise"
- Microtask queue empty, check task queue
- setTimeout callback executes: "2: setTimeout"
Complex Example: Mixing Everything
console.log('Start');
setTimeout(() => {
console.log('Timeout 1');
Promise.resolve().then(() => console.log('Promise in Timeout 1'));
}, 0);
Promise.resolve()
.then(() => {
console.log('Promise 1');
setTimeout(() => console.log('Timeout in Promise 1'), 0);
})
.then(() => console.log('Promise 2'));
setTimeout(() => console.log('Timeout 2'), 0);
console.log('End');
// Output:
// Start
// End
// Promise 1
// Promise 2
// Timeout 1
// Promise in Timeout 1
// Timeout in Promise 1
// Timeout 2
- Sync code: "Start", "End"
- Microtasks: "Promise 1", "Promise 2" (all microtasks before next macrotask)
- Macrotask 1: "Timeout 1" executes
- Microtask from Timeout 1: "Promise in Timeout 1"
- Macrotask 2: "Timeout in Promise 1"
- Macrotask 3: "Timeout 2"
Async/Await and the Event Loop
async/await is syntactic sugar over Promises, so it follows the same microtask rules:
console.log('1');
async function asyncFunc() {
console.log('2');
await Promise.resolve();
console.log('3'); // This is a microtask
}
asyncFunc();
Promise.resolve().then(() => console.log('4'));
console.log('5');
// Output:
// 1
// 2
// 5
// 3
// 4
await is scheduled as a microtask, just like .then(). The microtasks execute in the order they were queued.
Common Pitfalls
It doesn't! It's queued as a macrotask and waits for the call stack and all microtasks to clear.
function recursiveMicrotask() {
Promise.resolve().then(recursiveMicrotask);
}
recursiveMicrotask(); // Blocks the Event Loop!
This creates an infinite microtask queue, preventing macrotasks (like UI updates) from ever executing.
// Bad: blocks for 3 seconds
const start = Date.now();
while (Date.now() - start < 3000) {}
console.log('Done'); // UI is frozen during this time
Long-running synchronous code blocks the Event Loop, freezing the UI.
Best Practices
// Instead of processing 10,000 items at once:
async function processItems(items) {
for (let i = 0; i < items.length; i += 100) {
const batch = items.slice(i, i + 100);
await processBatch(batch);
// Yields to Event Loop between batches
}
}
// High priority: use Promise or queueMicrotask
queueMicrotask(() => {
// Executes before next macrotask
});
// Lower priority: use setTimeout
setTimeout(() => {
// Executes after microtasks
}, 0);
When debugging async issues, trace through:
- All synchronous code
- All microtasks (Promises, async/await)
- One macrotask (setTimeout, etc.)
- Repeat steps 2-3
Real-World Example: Debouncing
function debounce(func, delay) {
let timeoutId;
return function(...args) {
// Clear previous timeout (macrotask)
clearTimeout(timeoutId);
// Schedule new timeout (macrotask)
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// Usage: search input
const searchInput = document.querySelector('#search');
const debouncedSearch = debounce((query) => {
console.log('Searching for:', query);
// API call here
}, 300);
searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
Visualizing the Event Loop
Think of the Event Loop as a restaurant:
- Call Stack: The chef cooking one dish at a time
- Web APIs: Other kitchen staff (timers, dishwashers) working in parallel
- Microtask Queue: Urgent orders that must be done before the next main dish
- Task Queue: Regular orders waiting to be cooked
- Event Loop: The head chef deciding what to cook next
Key Takeaways
- JavaScript is single-threaded but non-blocking thanks to the Event Loop
- The call stack executes synchronous code
- Web APIs handle async operations in parallel
- Microtasks (Promises) have higher priority than macrotasks (setTimeout)
- The Event Loop processes: sync code → all microtasks → one macrotask → repeat
- Understanding the Event Loop helps you write better async code and debug timing issues
- Avoid blocking the main thread with long-running synchronous operations