Event Loop: Microtasks, Macrotasks & Render
Ek road, hazaar gaadiyaan — event loop traffic manage karta hai bhai.
JavaScript is single-threaded — it has exactly one call stack and can only do one thing at a time. Synchronous code runs top-to-bottom, functions push onto the stack and pop off when done.
Web APIs (browser) or libuv (Node.js) handle async operations on separate threads. When you call setTimeout, fetch, add event listeners, or do file I/O — the actual work happens outside V8, in the host environment.
When an async operation completes, its callback is placed in a queue. The event loop checks: is the call stack empty? If yes, pick the next callback from the queue and push it onto the stack.
console.log("1 — sync start");
setTimeout(() => {
console.log("4 — setTimeout callback");
}, 0); // handed to Web API — even 0ms!
console.log("2 — sync continues");
console.log("3 — sync end");
// Output: 1, 2, 3, 4
// Why? setTimeout goes to Web API, callback queued only after
// current call stack empties. Even 0ms waits for stack to clear.
The macrotask queue (also called task queue) holds callbacks from: setTimeout, setInterval, setImmediate (Node.js), I/O events, and UI events (click, scroll, etc.).
The event loop picks ONE macrotask at a time from the queue. After each macrotask completes, the entire microtask queue is drained (more on this next section).
Important details: The minimum setTimeout delay is ~4ms (HTML spec minimum after 5th nested timeout). setInterval drifts — each callback takes time, so the actual interval is longer than specified.
console.log("start");
setTimeout(() => console.log("timeout 1"), 0);
setTimeout(() => console.log("timeout 2"), 0);
setTimeout(() => console.log("timeout 3"), 0);
console.log("end");
// Output: start, end, timeout 1, timeout 2, timeout 3
// All 3 timeouts queued as macrotasks
// Event loop: runs sync (start, end) → picks timeout 1 → picks timeout 2 → picks timeout 3
// setInterval drift example
let last = Date.now();
setInterval(() => {
const now = Date.now();
console.log("Drift:", now - last - 100, "ms late");
last = now;
// Expensive work here adds to drift!
for (let i = 0; i < 1000000; i++) {} // simulate work
}, 100);
setTimeout(fn, 0) is often used to defer execution past the current synchronous code. But it is not truly 0ms — it is at minimum 4ms after 5+ nested levels. For true zero-delay microtask scheduling use Promise.resolve().then(fn) or queueMicrotask(fn).
The microtask queue has higher priority than the macrotask queue. It holds: Promise.then(), Promise.catch(), Promise.finally(), queueMicrotask(), and MutationObserver callbacks.
Microtasks run BEFORE any next macrotask — the entire microtask queue drains after each task. Even microtasks added during microtask processing are run immediately (before any macrotask).
This means: an infinite microtask loop will block the event loop forever! No macrotasks, no UI events, no rendering — page freezes.
console.log("1 — sync");
setTimeout(() => console.log("5 — macrotask"), 0);
Promise.resolve()
.then(() => console.log("3 — microtask 1"))
.then(() => console.log("4 — microtask 2")); // chained — runs after microtask 1
console.log("2 — sync");
// Output: 1, 2, 3, 4, 5
// Sync runs first: 1, 2
// Then ALL microtasks drain: 3, then 4 (chained micro)
// THEN macrotask: 5
// Danger: infinite microtask loop!
// function infiniteMicro() {
// Promise.resolve().then(infiniteMicro);
// }
// infiniteMicro(); // Blocks event loop forever!
// Page freezes — no macrotasks (UI events, renders) ever run!
The render step is when the browser actually updates what you see: style → layout → paint → composite. This does NOT happen after every macrotask — the browser is smart about batching visual updates.
requestAnimationFrame (rAF): your callback runs just before the next render step — synchronized to the display refresh rate (60fps = 16.6ms per frame). Perfect for visual updates and animations.
rAF is paused when the tab is hidden (saves battery and CPU) — but setTimeout continues firing in background tabs.
Full loop order: Macrotask → Microtasks → rAF callbacks → Render → next Macrotask
// rAF for smooth animation — synced to monitor refresh
let x = 0;
function animate() {
x += 2;
element.style.left = x + "px";
if (x < 300) {
requestAnimationFrame(animate); // schedule next frame
}
}
requestAnimationFrame(animate); // start
// Why NOT setTimeout for animation:
// setTimeout(animate, 16); // Not synced to display — causes jank
// Browser may render at 16.7ms, your timer fires at 16ms
// Result: some frames get 2 updates, some get 0 → visual stutter
// queueMicrotask — explicit microtask scheduling
console.log("1");
queueMicrotask(() => console.log("3 — microtask"));
setTimeout(() => console.log("4 — macrotask"), 0);
console.log("2");
// Output: 1, 2, 3, 4The complete event loop ordering for any JavaScript execution:
1. Run current synchronous code (call stack)
2. Drain entire microtask queue (Promise.then, queueMicrotask)
3. Run rAF callbacks (if render needed)
4. Render (style, layout, paint)
5. Pick ONE macrotask (setTimeout, UI event, I/O)
6. Go to step 2
Common interview question: order of console.log with a mix of sync, Promise, setTimeout. async/await creates a microtask continuation — equivalent to .then().
Node.js differences: process.nextTick runs before even Promise microtasks! setImmediate runs at the end of the I/O phase.
// The ultimate ordering test:
console.log("A"); // sync
setTimeout(() => console.log("F"), 0); // macrotask
Promise.resolve()
.then(() => {
console.log("C"); // microtask
setTimeout(() => console.log("G"), 0); // macrotask (queued from microtask)
})
.then(() => console.log("D")); // microtask chained
queueMicrotask(() => console.log("E")); // microtask
console.log("B"); // sync
// Output: A, B, C, D, E, F, G
// Sync: A, B
// Microtasks drain: C (Promise.then), D (chained), E (queueMicrotask)
// Note: G setTimeout queued but runs as macrotask, not now
// Macrotask: F (first setTimeout)
// Macrotask: G (setTimeout queued during C microtask)
// async/await is syntactic sugar over Promise.then
async function example() {
console.log("async start"); // sync (inside async fn)
await Promise.resolve(); // creates microtask continuation
console.log("after await"); // microtask
}
example();
console.log("after example() call"); // sync
// Output: "async start", "after example() call", "after await"
The event loop ordering question (A B C D E F G) is THE most asked JS interview question at senior level. Understand it from first principles: sync drains first, then ALL microtasks (including ones queued during microtask processing), then ONE macrotask, repeat.
Lo kar liya — Key Points:
- ✅ JS is single-threaded — one call stack. Async operations run in Web APIs (browser) or libuv (Node.js)
- ✅ Macrotask queue: setTimeout, setInterval, I/O, UI events — ONE macrotask per event loop turn
- ✅ Microtask queue: Promise.then, queueMicrotask — ENTIRE queue drains after every task before next macrotask
- ✅ Microtasks always run before macrotasks — even if setTimeout was registered first
- ✅ requestAnimationFrame runs before render, synced to display refresh — never use setTimeout for animations
- ✅ Full order: sync → microtasks → rAF → render → macrotask → microtasks → macrotask → repeat
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login