JavaScript Event Loop Explained
Understanding JavaScript's Event Loop, call stack, microtasks, and macrotasks for asynchronous programming
JavaScript is single-threaded, meaning it can only execute one task at a time. When one task blocks, subsequent computations cannot be executed. The Event Loop allows JavaScript to perform asynchronous operations.
Call Stack (Last In First Out)
When executing a function, it gets pushed onto the call stack. When the function completes execution, it gets popped from the call stack.
console.log(1);
console.log(2);
Execution Steps:
- Push
console.log(1)to call stack || call-stack: [console.log(1)] - Execute
console.log(1), then pop || call-stack: [] - Push
console.log(2)to call stack || call-stack: [console.log(2)] - Execute
console.log(2), then pop || call-stack: []
Microtasks vs Macrotasks
Task Types
- Microtasks:
Promise.then(),queueMicrotask(func),async/await - Macrotasks:
setTimeout(),setInterval(),setImmediate(), I/O operations
Execution Priority
When microtasks or macrotasks are encountered, they get pushed to their respective queues. When the call stack becomes empty, microtasks are processed first, then macrotasks.
Event Loop Example
Let’s trace through this complex example:
console.log(1);
setTimeout(() => console.log(2));
Promise.resolve().then(() => console.log(3));
Promise.resolve().then(() => setTimeout(() => console.log(4)));
Promise.resolve().then(() => console.log(5));
setTimeout(() => console.log(6));
console.log(7);
Step-by-Step Execution
- Push
console.log(1)to call stack → execute → pop - Push
setTimeout(() => console.log(2))to macrotask queue - Push
() => console.log(3)to microtask queue - Push
() => setTimeout(() => console.log(4))to microtask queue - Push
() => console.log(5)to microtask queue - Push
setTimeout(() => console.log(6))to macrotask queue - Push
console.log(7)to call stack → execute → pop - Call stack empty → Process microtask queue first
- Dequeue
console.log(3)→ execute → pop - Dequeue
setTimeout(() => console.log(4))→ push to macrotask queue - Dequeue
console.log(5)→ execute → pop - Microtask queue empty → Process macrotask queue
- Dequeue
console.log(2)→ execute → pop - Dequeue
console.log(6)→ execute → pop - Dequeue
console.log(4)→ execute → pop
Output: 1 7 3 5 2 6 4
Visual Representation
┌─────────────────┐
│ Call Stack │ ← JavaScript execution
└─────────────────┘
↑
┌─────────────────┐
│ Event Loop │ ← Coordinates task execution
└─────────────────┘
↗ ↖
┌──────────┐ ┌──────────┐
│Microtask │ │Macrotask │ ← Task queues
│ Queue │ │ Queue │
└──────────┘ └──────────┘
Key Rules
- Call stack has priority: Nothing from queues executes while call stack isn’t empty
- Microtasks always go first: All microtasks execute before any macrotask
- One macrotask per cycle: After each macrotask, check for microtasks again
Common Patterns
Promise Chain vs setTimeout
// This executes in order: A, C, B
console.log('A'); // Synchronous - immediate
setTimeout(() => console.log('B'), 0); // Macrotask - deferred
Promise.resolve().then(() => console.log('C')); // Microtask - priority
Nested Promises and Timeouts
setTimeout(() => {
console.log('timeout1');
Promise.resolve().then(() => console.log('promise1'));
}, 0);
setTimeout(() => {
console.log('timeout2');
Promise.resolve().then(() => console.log('promise2'));
}, 0);
// Output: timeout1, promise1, timeout2, promise2
Practical Implications
1. UI Responsiveness
// Bad - blocks UI
for (let i = 0; i < 1000000; i++) {
// heavy computation
}
// Good - allows UI updates
function processChunk(index = 0) {
const chunkSize = 1000;
// Process chunk
if (index < 1000000) {
setTimeout(() => processChunk(index + chunkSize), 0);
}
}
2. Promise Resolution Order
Promise.resolve().then(() => {
console.log('promise1');
return Promise.resolve();
}).then(() => {
console.log('promise2');
});
Promise.resolve().then(() => {
console.log('promise3');
});
// Output: promise1, promise3, promise2
Best Practices
- Understand execution order for debugging async code
- Use microtasks (Promises) for high-priority async operations
- Use macrotasks (setTimeout) for lower-priority or UI-related operations
- Avoid blocking the call stack with heavy synchronous operations
- Test async code thoroughly as execution order can be counterintuitive
Common Misconceptions
- ❌
setTimeout(fn, 0)executes immediately - ❌ Promises are synchronous
- ❌ All async operations have the same priority
- ✅ Event loop coordinates between sync and async code
- ✅ Microtasks have higher priority than macrotasks
- ✅ Understanding this helps debug timing-related bugs
The Event Loop is fundamental to JavaScript’s asynchronous nature and understanding it is crucial for writing efficient, non-blocking applications.