Event Loop & Concurrency
Call stack, task queue, microtasks, and how JavaScript handles asynchronous operations
How JavaScript Handles Async Operations
JavaScript is single-threaded, meaning it can only execute one piece of code at a time. Yet it handles async operations like network requests and timers seamlessly. This is possible thanks to the event loop, which coordinates execution between the call stack and task queues.
Key Components
- Call Stack — Where JavaScript executes functions (LIFO)
- Web APIs — Browser-provided APIs (setTimeout, fetch, DOM events)
- Task Queue (Macrotask) — Holds callbacks from setTimeout, setInterval, I/O
- Microtask Queue — Holds Promise callbacks, queueMicrotask, MutationObserver
- Event Loop — Continuously checks if stack is empty, then processes queues
The Call Stack
JavaScript uses a call stack to track function execution:
function multiply(a, b) {
return a * b;
}
function square(n) {
return multiply(n, n);
}
function printSquare(n) {
const result = square(n);
console.log(result);
}
printSquare(4);
// Call Stack progression:
// 1. printSquare(4)
// 2. printSquare(4) → square(4)
// 3. printSquare(4) → square(4) → multiply(4, 4)
// 4. printSquare(4) → square(4) ← returns 16
// 5. printSquare(4) ← returns 16
// 6. Empty (done)
setTimeout and the Task Queue
console.log("Start");
setTimeout(() => {
console.log("Timeout");
}, 0);
console.log("End");
// Output:
// "Start"
// "End"
// "Timeout"
// Why? Even with 0ms delay:
// 1. console.log("Start") runs
// 2. setTimeout callback → Task Queue (not call stack)
// 3. console.log("End") runs
// 4. Stack empty → Event loop checks Task Queue
// 5. Callback runs → "Timeout"
Microtasks vs Macrotasks
Microtasks have higher priority than macrotasks (tasks). All microtasks run before the next macrotask:
console.log("1. Script start");
setTimeout(() => {
console.log("4. setTimeout (macrotask)");
}, 0);
Promise.resolve()
.then(() => console.log("3. Promise (microtask)"));
console.log("2. Script end");
// Output order:
// 1. Script start
// 2. Script end
// 3. Promise (microtask)
// 4. setTimeout (macrotask)
| Microtasks (High Priority) | Macrotasks (Lower Priority) |
|---|---|
| Promise.then/catch/finally | setTimeout |
| queueMicrotask() | setInterval |
| MutationObserver | setImmediate (Node) |
| async/await (after await) | I/O operations |
Event Loop Algorithm
// Simplified Event Loop
while (true) {
// 1. Execute all synchronous code in call stack
// 2. Execute ALL microtasks
while (microtaskQueue.length > 0) {
executeMicrotask(microtaskQueue.shift());
}
// 3. Execute ONE macrotask (if any)
if (macrotaskQueue.length > 0) {
executeMacrotask(macrotaskQueue.shift());
}
// 4. Render updates (if needed, ~60fps)
// 5. Repeat
}
Complex Example
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve()
.then(() => {
console.log("3");
setTimeout(() => console.log("4"), 0);
})
.then(() => console.log("5"));
Promise.resolve().then(() => console.log("6"));
console.log("7");
// Output: 1, 7, 3, 6, 5, 2, 4
// Explanation:
// Sync: 1, 7
// Microtasks: 3, 6, 5 (Promise chains)
// Macrotask: 2 (first setTimeout)
// Macrotask: 4 (setTimeout added during microtask)
async/await and the Event Loop
async function asyncExample() {
console.log("1. Async start");
await Promise.resolve();
// Everything after await is a microtask
console.log("3. After await");
}
console.log("0. Script start");
asyncExample();
console.log("2. Script end");
// Output:
// 0. Script start
// 1. Async start
// 2. Script end
// 3. After await
// The code after 'await' runs as a microtask
Blocking the Event Loop
// ❌ BAD: Long-running synchronous code blocks everything
function blockLoop() {
const start = Date.now();
while (Date.now() - start < 3000) {
// Blocks for 3 seconds
}
console.log("Done blocking");
}
// UI freezes, no callbacks run, no rendering
blockLoop();
// ✅ BETTER: Break up work
function processChunk(items, index = 0, chunkSize = 100) {
const end = Math.min(index + chunkSize, items.length);
for (let i = index; i < end; i++) {
// Process item
}
if (end < items.length) {
// Yield to event loop, then continue
setTimeout(() => processChunk(items, end, chunkSize), 0);
}
}
requestAnimationFrame
// Runs before the next repaint (~60fps)
function animate() {
// Update animation state
element.style.left = position + "px";
// Schedule next frame
requestAnimationFrame(animate);
}
📚 Learn More
requestAnimationFrame(animate);
// Priority: Microtasks → rAF → Macrotasks
💡 Key Takeaways
- • JavaScript is single-threaded but handles async via the event loop
- • Microtasks (Promises) run before macrotasks (setTimeout)
- • All microtasks run before the next macrotask
- • Long synchronous operations block the event loop and freeze the UI
- • Use setTimeout or requestAnimationFrame to break up heavy work
- • async/await code after
awaitruns as a microtask