Difference between microtask and macrotask queue with a real example.
Microtasks (Promise reactions, queueMicrotask, MutationObserver) are drained ENTIRELY after each task and before rendering. Macrotasks (setTimeout, setInterval, DOM events, I/O) run ONE per loop iteration. So all pending Promise callbacks run before the next setTimeout — and an unbounded microtask chain can starve macrotasks and block paint.
Both are queues of callbacks waiting to run, but they differ in what feeds them and how aggressively the event loop drains them.
The distinction
| Microtask queue | Macrotask queue (task queue) | |
|---|---|---|
| Fed by | Promise .then/catch/finally, await continuations, queueMicrotask, MutationObserver | setTimeout, setInterval, DOM events, I/O, MessageChannel, postMessage |
| Drained | Entire queue, every iteration, before rendering | One task per iteration |
| Priority | Higher — always runs before the next macrotask | Lower |
After each macrotask, the loop drains all microtasks (including any added during the drain) before it even considers the next macrotask or a render.
Real example
console.log("script start");
setTimeout(() => console.log("setTimeout"), 0);
Promise.resolve()
.then(() => console.log("promise 1"))
.then(() => console.log("promise 2"));
queueMicrotask(() => console.log("queueMicrotask"));
console.log("script end");Output:
script start
script end <- sync code first
promise 1 <- microtasks drain...
queueMicrotask
promise 2 <- (chained .then queued during the drain — still this cycle)
setTimeout <- ...then one macrotaskThe whole microtask queue empties before setTimeout (a macrotask) gets a turn.
Why the difference matters
- Ordering bugs — code that "works" because a Promise happens to resolve before a timer is fragile.
- Starvation — a microtask that keeps queuing microtasks never yields. The macrotask queue and rendering are starved → the page freezes:
``js function loop() { Promise.resolve().then(loop); } // freezes the tab ``
- Batching — frameworks (React, Vue) use microtasks to batch updates so they apply before the browser paints.
- If you need to yield to rendering/input, use a macrotask (
setTimeout,MessageChannel), not a microtask.
Senior framing
The senior point: microtasks are for "finish this logical unit of work before the browser does anything else" (consistency); macrotasks are for "let the browser breathe — render, handle input — then continue" (responsiveness). Choosing the wrong one causes either stale UI or jank.
Follow-up questions
- •How would you yield to the browser to let it render mid-computation?
- •Why do frameworks batch state updates in microtasks?
- •Can you starve the macrotask queue with microtasks? How?
Common mistakes
- •Assuming setTimeout(fn,0) runs before a resolved Promise's .then.
- •Writing recursive Promise chains that starve rendering.
- •Relying on accidental ordering between a timer and a Promise.
Edge cases
- •Microtasks queued during the microtask drain run in the same cycle, not the next.
- •Between microtask drains the browser may render; between microtasks it never does.
Real-world examples
- •React batching updates, Vue's nextTick, Promise-based data flows resolving before timers.