Skip to main content

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
  • queueMicrotask runs before setTimeout(0), in the same microtask checkpoint as Promise.then
  • Both fire in FIFO order, so queueMicrotask queued before Promise.then runs 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

javascript
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. setTimeout

Sync 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:

javascript
setTimeout(() => console.log("Timeout"), 0); queueMicrotask(() => console.log("Micro")); // runs first // Output: Micro, Timeout

Microtasks 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:

javascript
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:

javascript
try { queueMicrotask(() => { throw new Error("Boom"); }); } catch (e) { console.log("Caught"); // never runs } // Error fires asynchronously, after the current stack

Wrap the try/catch inside the callback, not around queueMicrotask itself.

Mixing up Node.js and browser priority:

javascript
process.nextTick(() => console.log("nextTick")); queueMicrotask(() => console.log("queueMicrotask")); // Output: nextTick, queueMicrotask

In 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 via queueMicrotask after the reactivity system flushes
  • React 18 - the scheduler uses queueMicrotask for passive effects (post-commit, pre-paint), and automatic batching now extends to native async contexts like fetch
  • Angular / Zone.js - patches queueMicrotask to track async zones and trigger change detection
  • Preact - flushSync defers via queueMicrotask for 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

javascript
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 // timeout

queueMicrotask fires before Promise.then here because it was registered first. Both live in the same microtask queue, processed in order.

Intermediate: batching DOM writes

javascript
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 two

This 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

javascript
const loopMicrotask = () => { console.log("still running..."); queueMicrotask(loopMicrotask); // re-queues itself }; queueMicrotask(loopMicrotask); // "still running..." logs forever // setTimeout callbacks never fire // browser paint never happens

I 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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?