How does the event loop prioritize microtasks vs macrotasks?
The event loop drains the entire microtask queue after every macrotask, then renders. Misunderstanding this order causes subtle async bugs.
JavaScript is single-threaded, so all work runs on one call stack. The event loop is the runtime mechanism that decides what runs next when the stack is empty. Its cycle is strict and deterministic: pick one macrotask, run it to completion, drain the entire microtask queue, then check whether to perform a rendering pass (style → layout → paint → composite), then repeat. Understanding this ordering is the difference between code that feels snappy and code with mysterious "off-by-one tick" bugs.
Macrotasks (a.k.a. "tasks" in the HTML spec) include setTimeout, setInterval, setImmediate (Node only), I/O callbacks, UI events like click and scroll, MessageChannel.postMessage, and fetch resolution at the network layer. Each macrotask comes from a task source, and the spec gives the browser freedom to pick which source to drain first — which is why mixing setTimeout and postMessage can give surprising ordering.
Microtasks include Promise.then/catch/finally callbacks, queueMicrotask, MutationObserver callbacks, and in Node, process.nextTick (which actually has even higher priority than promise microtasks — it runs before the regular microtask queue is drained). Microtasks are designed for "finish this synchronous-ish work before yielding," so they piggy-back on the current task.
The critical rule: microtasks always run before the next macrotask, and microtasks queued during the drain are appended to and processed in the same drain. This recursion is unbounded — an infinite chain of Promise.resolve().then(...) will starve the UI because the loop never reaches the rendering step. Contrast with setTimeout(fn, 0) recursion, which still yields between iterations.
Rendering is folded into the macrotask boundary, but not after every task. The browser opportunistically renders roughly once per display refresh (typically 16.6ms on a 60Hz monitor). requestAnimationFrame callbacks fire before style/layout/paint of that frame, after microtasks have drained — they're the right place to read layout-derived values or schedule pre-paint mutations. requestIdleCallback runs only if the frame has spare budget.
A few practical consequences: (1) await is a microtask checkpoint — code after an await runs in a microtask, not synchronously, even if the awaited promise is already resolved. (2) Batching state updates in React's concurrent mode leverages microtasks to coalesce updates within a tick. (3) MutationObserver lets you observe DOM changes without forcing a sync layout, because the callback is a microtask, not a synchronous event handler. (4) Long-running JS work blocks both rendering and input, so for big computations use scheduler.postTask (Chromium), isInputPending, or chunk work across setTimeout(_, 0) to give the renderer a chance.
Node's loop has additional phases (timers, pending callbacks, poll, check, close) and runs process.nextTick + microtasks between every phase transition, which is why nextTick can starve I/O if abused.
Code
Follow-up questions
- •What happens if a microtask throws?
- •How does Node's process.nextTick differ?
- •Why can a long microtask chain block rendering?
Common mistakes
- •Assuming setTimeout(fn, 0) runs before a pending Promise.then.
- •Believing microtasks yield between each callback — they don't.
- •Mixing requestAnimationFrame and microtasks expecting them to interleave predictably.
Performance considerations
- •Long microtask chains can starve rendering and input — measure with the Performance panel.
- •Use scheduler.postTask (Chromium) or yieldToMain patterns to break up CPU work.
Edge cases
- •An unhandled promise rejection inside a microtask schedules `unhandledrejection` as another microtask.
- •MutationObserver callbacks are microtasks — DOM mutation in a tight loop can blow up the queue.
Real-world examples
- •React batching uses microtasks (in concurrent mode) to coalesce state updates within a single tick.