Code of the Day
AdvancedPerformance & Internals

Microtasks and the event loop

Why promise callbacks run before timers — the event loop in detail.

JavaScript / TSAdvanced9 min read
Recommended first
By the end of this lesson you will be able to:
  • Distinguish the macrotask and microtask queues
  • Predict the order of sync, promise, and timer callbacks
  • Avoid blocking the event loop

The async lesson introduced the . This goes one level deeper, into a detail that explains a lot of surprising ordering: JavaScript has two queues of pending work, and they don't have equal priority.

Two queues

After each piece of synchronous code finishes, the event loop drains work in a specific order:

  • — promise callbacks (.then, the continuation after await), queueMicrotask. The loop drains the entire microtask queue before doing anything else.
  • MacrotaskssetTimeout, I/O callbacks, events. Only one is processed per loop iteration, after all microtasks are clear.

So promises always run ahead of timers, even setTimeout(fn, 0).

Predicting the order

This is the classic interview puzzle — and now it makes sense:

console.log("1");
setTimeout(() => console.log("2"), 0);   // macrotask
Promise.resolve().then(() => console.log("3"));   // microtask
console.log("4");
// prints: 1, 4, 3, 2

1 and 4 are synchronous. Then the loop drains microtasks (3) before taking the timer macrotask (2). Promise continuations always win the race against timers.

Don't block the loop

Because it's all one thread (the async lesson), a long synchronous computation freezes everything — no other callback, no rendering, nothing runs until it finishes:

// Blocks the whole thread; the UI is frozen for the duration.
for (let i = 0; i < 5_000_000_000; i++) {}

For heavy CPU work, move it off the main thread — into a Web Worker (the same mechanism the in-browser runners on this site use). The main thread stays responsive while the worker computes.

An endless chain of microtasks can starve macrotasks — if promise callbacks keep scheduling more promise callbacks, timers and rendering never get a turn. Order matters, and so does not monopolising either queue.

Where to go next

The event loop governs when code runs; memory governs how long values live. Next: memory and garbage collection.

Finished reading? Mark it complete to track your progress.

On this page