Microtasks vs Macrotasks
(The Event Loop topic covers how these queues are processed; this topic catalogs what goes in each queue and compares them directly.)
🎯 What you'll learn
- The Core Rule
- What Goes Where
- Side-by-Side Comparison
- Direct Comparison Example
queueMicrotask— Scheduling a Microtask Directly
(The Event Loop topic covers how these queues are processed; this topic catalogs what goes in each queue and compares them directly.)
1The Core Rule
All pending microtasks run before the next macrotask, every single time — no exceptions. This one rule explains almost every "surprising" async ordering question.
2What Goes Where
| Microtask sources | Macrotask sources |
|---|---|
Promise.prototype.then/catch/finally callbacks | setTimeout / setInterval |
queueMicrotask(fn) | setImmediate (Node.js only) |
async function code after an await | I/O callbacks (fs.readFile, network events) |
MutationObserver callbacks (browser) | UI events (click, keydown) |
requestAnimationFrame (browser — technically its own category, but runs after microtasks, before the next paint) |
3Side-by-Side Comparison
| Microtask | Macrotask | |
|---|---|---|
| Priority | higher — always drained first | lower — one runs per loop iteration |
| Queue draining | entire queue drained per pass, including new entries added during draining | one task per pass, then microtasks are re-checked |
| Typical use | Promise resolution, reacting quickly to already-available data | timers, scheduling work for "later," I/O completion |
| Can starve the other? | Yes — an infinite microtask chain can starve all macrotasks (see the Event Loop topic) | No — a macrotask cannot block microtasks from running; each macrotask still yields back to microtask draining afterward |
4Direct Comparison Example
console.log("1");
setTimeout(() => console.log("2 (macrotask)"), 0);
setTimeout(() => console.log("3 (macrotask)"), 0);
Promise.resolve().then(() => console.log("4 (microtask)"));
Promise.resolve().then(() => console.log("5 (microtask)"));
console.log("6");Output: 1, 6, 4, 5, 2, 3 — both microtasks run before either macrotask, regardless of registration order between the two categories.
5queueMicrotask — Scheduling a Microtask Directly
console.log("A");
queueMicrotask(() => console.log("B (explicit microtask)"));
console.log("C");
// A, C, BUseful when you want to defer work just slightly — past the current synchronous code — without the extra overhead or minimum-delay behavior of setTimeout.
6async/await Uses Microtasks Internally
async function example() {
console.log("1: sync part of async function");
await null; // ANYTHING after this point is scheduled as a microtask
console.log("3: resumed as a microtask");
}
example();
console.log("2: continues synchronously after calling example()");
// 1, 2, 3This is why awaiting even an already-resolved value still yields to the microtask queue — the code after await never runs perfectly synchronously, even for instantly-available values.
7Node.js Extra Detail: process.nextTick
Node.js has an additional queue, process.nextTick, that runs with even higher priority than Promise microtasks — it's drained completely before the microtask queue on each pass. This is Node-specific and does not exist in browsers.
Promise.resolve().then(() => console.log("promise microtask"));
process.nextTick(() => console.log("nextTick"));
// nextTick, then promise microtask — in Node.js onlyUnder the HoodThe Microtask Checkpoint Is a Real, Callable Operation▾
"Draining the microtask queue" isn't just a conceptual step in a diagram — it corresponds to a concrete operation engines expose internally, called a microtask checkpoint (V8's embedder API calls this PerformMicrotaskCheckpoint). Whatever is embedding the JS engine — a browser, Node — is responsible for calling this checkpoint at the right moments; V8 itself does not decide on its own when to run it.
Who calls the checkpoint, and when
- Node.js: the checkpoint runs after each callback invocation and between each of the six libuv phases described in the Event Loop topic's "Under the Hood" section — this is precisely why microtasks interleave as tightly as they do with Node's phase structure.
- Browsers: the checkpoint runs after each task completes, and additionally at specific points during certain DOM operations — which is why microtasks can sometimes run mid-way through, e.g., a sequence of synchronous DOM mutations, not only at the very end of a script.
Why this architecture matters: microtasks are an engine-level queue, macrotasks are host-defined
setTimeout, setInterval, and I/O callbacks are not part of the JavaScript language specification at all — they're provided by the host environment (the browser's Web APIs, or Node's libuv bindings), which is why their exact scheduling mechanics (timer heaps, libuv phases) differ between environments. The microtask queue, by contrast, genuinely is part of the ECMAScript specification itself (HostEnqueuePromiseJob notwithstanding, the ordering guarantees around draining are spec-mandated) — this is why microtask-vs-macrotask priority behaves identically across every conformant JS environment, while the fine details of macrotask scheduling (libuv's six phases vs a browser's task queue) genuinely differ by host.
⚠️ Common Pitfalls
- Assuming
setTimeout(fn, 0)is "as fast as possible" — a Promise microtask is always faster; use microtasks for genuinely urgent follow-up work. - Forgetting
awaitalways defers, even for synchronous-looking values —await 5still yields to the microtask queue before continuing. - Overusing
process.nextTickin Node — because it preempts even Promise microtasks, heavy use can starve Promise resolution, similar to the microtask-starves-macrotask problem in the Event Loop topic. - Assuming
requestAnimationFramebehaves like a microtask — it's scheduled relative to the browser's paint cycle, not the microtask/macrotask split; treat it as its own category.
📋 Quick Reference
microtask: Promise .then/.catch/.finally, queueMicrotask, code after await
macrotask: setTimeout, setInterval, setImmediate (Node), I/O, UI events
priority: ALL microtasks drain before the NEXT macrotask, every pass
Node only: process.nextTick > Promise microtasks > macrotasks🎤 Interview Q&A
Q1What are microtasks and macrotasks?core▾
Macrotasks (tasks): script execution, setTimeout/setInterval, I/O, UI events, MessageChannel. Microtasks: promise reactions, queueMicrotask, MutationObserver (and process.nextTick in Node, with higher priority). All microtasks run after each task before the next task.
- Which runs first after sync code? Microtasks.
Q2Predict: setTimeout(() => log('t')); Promise.resolve().then(() => log('p1')).then(() => log('p2')); queueMicrotask(() => log('q')).core▾
p1, q, p2, t: p1 and q are queued in order; p2 is queued when p1 runs; the timer runs after all microtasks.
- Why is p2 after q? It's queued only after p1 executes.
Q3Where does await resume?core▾
The continuation after await is scheduled as a microtask when the awaited promise settles, so code after await runs after the current synchronous code.
- Does await on a non-promise yield? Yes, it still defers to a microtask.
Q4When would you use queueMicrotask?senior▾
To defer work until after the current operation but before rendering or other tasks, e.g. batching notifications in a library while keeping consistent state.
- Risk? Starving rendering if overused.
Q5How does process.nextTick relate in Node?core▾
Node drains the nextTick queue before the promise microtask queue; excessive nextTick recursion can starve I/O.
- Guidance? Prefer queueMicrotask/setImmediate in new code.
🛠️ Practice exercises
Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.