Event Loop & Concurrency
Call stack, task queues, microtasks, concurrency model
The Call Stack
JavaScript runs one thing at a time. The call stack tracks execution.
function square(n) {
return n * n
}
function logSquare(n) {
console.log(square(n))
}
logSquare(4) // Stack: [square, logSquare, main]
Blocking the stack blocks everything (including rendering):
const start = Date.now()
while (Date.now() - start < 5000) {} // Blocks for 5 seconds
Task Queue (Macrotasks)
setTimeout, I/O, UI events — one per event loop cycle.
console.log(1)
setTimeout(() => console.log(2), 0)
console.log(3)
// 1, 3, 2
Microtask Queue
Promise.then/.catch/.finally, queueMicrotask. Runs after current task, before next macrotask.
console.log(1)
setTimeout(() => console.log(2), 0)
Promise.resolve().then(() => console.log(3))
console.log(4)
// 1, 4, 3, 2
Microtasks drain fully — nested microtasks run before next macrotask:
setTimeout(() => console.log(1), 0)
Promise.resolve().then(() => {
console.log(2)
queueMicrotask(() => console.log(3))
})
console.log(4)
// 4, 2, 3, 1
Predict This Order
console.log(1)
setTimeout(() => console.log(2), 0)
Promise.resolve().then(() => {
console.log(3)
setTimeout(() => console.log(4), 0)
Promise.resolve().then(() => console.log(5))
})
console.log(6)
// 1, 6, 3, 5, 2, 4
Event Loop Diagram
BROWSER: sync → drain microtasks → rAF → paint → macrotask
NODE: timers → I/O → poll → check(setImmediate) → close
(microtasks drain between every phase)
Starving the Loop
// BAD — blocks everything
items.forEach((item) => heavy(item))
// BETTER — yield every N items
for (let i = 0; i < items.length; i++) {
heavy(items[i])
if (i % 50 === 0) await new Promise((r) => setTimeout(r, 0))
}
See It In Action
Run this to see microtask vs macrotask execution order in real-time:
1console.log(1)2setTimeout(() => console.log(2), 0)3Promise.resolve().then(() => console.log(3))4Promise.resolve().then(() => {5console.log(4)6setTimeout(() => console.log(5), 0)7})8setTimeout(() => console.log(6), 0)9console.log(7)10 11// Expected output order: 1, 7, 3, 4, 2, 6, 5Synchronous code (1, 7) runs first. Then microtasks (Promise callbacks — 3, 4) drain before the next macrotask. Macrotasks (setTimeout — 2, 6, 5) each go to the back of the macrotask queue. Nested microtask (4 → queueMicrotask'd 8) would also drain before any remaining macrotask.
See It In Action
1console.log(1)2setTimeout(() => { console.log(2); Promise.resolve().then(() => console.log(3)) }, 0)3Promise.resolve().then(() => { console.log(4); setTimeout(() => console.log(5), 0) })4setTimeout(() => console.log(6), 0)5queueMicrotask(() => { console.log(7); queueMicrotask(() => console.log(8)) })6console.log(9)7// Expected output: 1, 9, 4, 7, 8, 2, 6, 3, 5Step by step: sync code 1,9 first. Then microtask queue drains: 4, nested queueMicrotask 7, which queues 8 — 8 runs before next macrotask. Macrotasks: first setTimeout logs 2, its microtask logs 3; remaining setTimeout logs 6; last setTimeout logs 5.