How to add a task to microtask queue with queueMicrotask
queueMicrotask(callback) schedules a function in the microtask queue, which the JavaScript event loop processes after the current call stack empties but before any pending macrotasks like setTimeout callbacks.
Theory
TL;DR
- Think of the event loop as a restaurant cashier: macrotasks are full orders in the main line, microtasks are quick "napkin refills" handled right after the current customer, before the next full order starts
queueMicrotaskruns beforesetTimeout(0), in the same microtask checkpoint asPromise.then- Both fire in FIFO order, so
queueMicrotaskqueued beforePromise.thenruns first - Use it for deferred work that should not yield to the macrotask queue
- Not for heavy compute: long microtask chains block paint
Quick example
console.log("1. Script start");
setTimeout(() => console.log("4. setTimeout"), 0);
Promise.resolve().then(() => console.log("3. Promise.then"));
queueMicrotask(() => console.log("2. queueMicrotask"));
console.log("5. Script end");
// Output:
// 1. Script start
// 5. Script end
// 2. queueMicrotask
// 3. Promise.then
// 4. setTimeoutSync code runs first. Then all microtasks drain in the order they were queued. setTimeout fires last, even with a 0 delay.
Key difference from Promise.resolve().then()
Both queueMicrotask and Promise.resolve().then() push work into the microtask queue. The wrapper is what differs. Promise.then auto-unwraps returned Promises and routes rejections through .catch. queueMicrotask is a plain callback with no rejection handling: if the callback throws, the error surfaces as an unhandled exception after the current stack, not as a rejected Promise. Pick queueMicrotask when you need post-sync timing without the Promise machinery around it.
When to use
- Batch DOM writes: defer a style update until all sync state changes finish, then write to the DOM once
- Framework state sync: run an effect after reactive state settles, but before the browser paints
- Post-sync cleanup: run teardown code without holding up macrotasks
- Replace
setTimeout(0)for timing: when you need true "after current task" without the 4ms timer floor browsers enforce - Avoid it for heavy computation or anything involving I/O. Those belong in macrotasks or Web Workers
How the event loop handles microtasks
In V8 (Chrome and Node.js), queueMicrotask pushes the callback into a MicrotaskQueue tied to the current JavaScript global scope - a linked list of internal Microtask objects. After the current macrotask finishes, the event loop enters the RunMicrotasks phase. It iterates the queue until empty, including any new microtasks added during that drain. Only then does it move to paint or pick up the next macrotask.
Browsers implement this via the HTML spec's "microtask checkpoint". Node.js integrates with libuv but follows the same priority. One difference: in Node.js, process.nextTick drains before queueMicrotask and Promise.then, so it runs at a higher priority despite being called a "microtask" in some docs.
Common mistakes
Assuming setTimeout fires first:
setTimeout(() => console.log("Timeout"), 0);
queueMicrotask(() => console.log("Micro")); // runs first
// Output: Micro, TimeoutMicrotasks drain before any macrotask. If you need setTimeout to go first, the two should not be mixed this way.
Recursive queueMicrotask with no exit condition:
function recurse() {
queueMicrotask(recurse); // no base case
}
queueMicrotask(recurse);
// Event loop never exits the microtask phase. Paint freezes.The queue drains until empty. If new microtasks keep arriving, it never leaves. V8 does not auto-stop this. Add a counter or switch to setTimeout for work that needs to yield.
Throwing inside and expecting a try/catch outside:
try {
queueMicrotask(() => { throw new Error("Boom"); });
} catch (e) {
console.log("Caught"); // never runs
}
// Error fires asynchronously, after the current stackWrap the try/catch inside the callback, not around queueMicrotask itself.
Mixing up Node.js and browser priority:
process.nextTick(() => console.log("nextTick"));
queueMicrotask(() => console.log("queueMicrotask"));
// Output: nextTick, queueMicrotaskIn Node.js, process.nextTick has higher priority than queueMicrotask. In browsers, process.nextTick does not exist. For cross-runtime code, use queueMicrotask - it follows the spec in both environments.
Real-world usage
- Vue 3 -
nextTick()queues DOM updates viaqueueMicrotaskafter the reactivity system flushes - React 18 - the scheduler uses
queueMicrotaskfor passive effects (post-commit, pre-paint), and automatic batching now extends to native async contexts likefetch - Angular / Zone.js - patches
queueMicrotaskto track async zones and trigger change detection - Preact -
flushSyncdefers viaqueueMicrotaskfor update batching - Node.js undici - the HTTP client queues response parsing as a microtask after I/O completes
Follow-up questions
Q: What is the output order of sync code, queueMicrotask, Promise.then, and setTimeout?
A: Sync code runs first. Then all microtasks drain in FIFO order (queueMicrotask and Promise.then share the same queue, whichever was registered first runs first). setTimeout fires last.
Q: What is the difference between queueMicrotask and Promise.resolve().then()?
A: Both schedule work in the microtask queue at the same priority level. Promise.then handles rejections and unwraps returned Promises automatically. queueMicrotask is a simpler callback with no Promise wrapping and no rejection routing.
Q: Does queueMicrotask block rendering?
A: Yes. Microtasks drain before the browser paints. A long chain or a recursive queueMicrotask will freeze the UI. Use requestIdleCallback or setTimeout if the work needs to yield to the render pipeline.
Q: How does process.nextTick relate to queueMicrotask in Node.js?
A: Both are microtask-like, but process.nextTick has higher internal priority and drains first. queueMicrotask follows the WHATWG spec and behaves identically to browsers. For code running in both environments, queueMicrotask is the portable choice.
Q: How does V8 implement the microtask queue internally?
A: V8 uses a MicrotaskQueue stored as a linked list of v8::internal::Microtask objects, tied to the current JavaScript global scope. After each macrotask, the event loop calls MaybeResolvingMicrotaskQueue, which iterates and runs each entry, including newly added ones, until the list is empty.
Examples
Basic: execution order walkthrough
console.log("sync 1");
queueMicrotask(() => console.log("microtask"));
Promise.resolve().then(() => console.log("promise"));
setTimeout(() => console.log("timeout"), 0);
console.log("sync 2");
// sync 1
// sync 2
// microtask <- registered before Promise.then, fires first
// promise
// timeoutqueueMicrotask fires before Promise.then here because it was registered first. Both live in the same microtask queue, processed in order.
Intermediate: batching DOM writes
let count = 0;
function increment() {
count += 1;
count += 1; // multiple sync state mutations
queueMicrotask(() => {
// single DOM write after all sync updates settle
document.getElementById("counter").textContent = count;
console.log("DOM updated:", count); // 2
});
}
increment();
// One layout calculation instead of twoThis is the same pattern Vue 3's nextTick and React's scheduler use. Group your state mutations synchronously, then write to the DOM once inside the microtask. The browser sees only one change and runs one layout pass.
Advanced: the infinite microtask trap
const loopMicrotask = () => {
console.log("still running...");
queueMicrotask(loopMicrotask); // re-queues itself
};
queueMicrotask(loopMicrotask);
// "still running..." logs forever
// setTimeout callbacks never fire
// browser paint never happensI saw this exact pattern cause a full page hang during a code review, where someone tried to poll a condition with queueMicrotask instead of setInterval. The event loop drains microtasks completely before moving on. If each microtask adds another, it never moves on. Always add a counter as a guard or use setTimeout for polling work that needs to yield.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.