Chapter 2.5☕ 28 min read

Event Loop: Microtasks, Macrotasks & Render

Ek road, hazaar gaadiyaan — event loop traffic manage karta hai bhai.

01The Call Stack & Web APIs

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.
Web APIs are NOT part of V8. V8 is just the JS engine — it has no setTimeout, no fetch, no DOM. These are provided by the HOST ENVIRONMENT (browser or Node.js). V8 just executes JS code and calls callbacks when the host tells it to.
02Macrotask Queue (Task Queue)

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) vs Microtasks:
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).
03Microtask Queue — Highest Priority

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!
This is why Promise.then() always executes BEFORE setTimeout(fn, 0) — even if the setTimeout was registered first. Microtasks drain completely after every task. Angular's ChangeDetection is a microtask — that's why template updates happen synchronously after an async operation resolves.
04Render Step & requestAnimationFrame

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, 4
05Complete Event Loop: Putting It Together

The 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 #1 Interview Question:
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
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase