Skip to main content

The queueMicrotask() function

queueMicrotask(callback) puts the given function into the microtask queue, and it runs right after the current synchronous code finishes, but before any macrotask. It is the simplest way to defer an action to "the next moment the stack is empty" without creating a promise for it.

Theory

TL;DR

  • queueMicrotask(fn) adds fn to the Microtask Queue and returns undefined.
  • The microtask runs once the Call Stack is empty, but before the first macrotask (setTimeout, setInterval, events, I/O).
  • The behaviour is identical to Promise.resolve().then(fn), only without the cost of creating a Promise object.
  • The Event Loop drains the microtask queue completely, including microtasks added while it is being drained.
  • Adding microtasks endlessly blocks the Event Loop: no macrotask will ever get its turn.
  • Standardised in ECMAScript 2019, supported by all modern browsers and Node.js.

Quick example

javascript
console.log('A'); queueMicrotask(() => console.log('B')); console.log('C'); // Output: // A // C // B

First all of the synchronous code runs (A, then C). Then the Event Loop sees that the stack is empty and runs the microtask (B) before moving on to macrotasks.

What queueMicrotask() actually does

The signature is as simple as it gets:

javascript
queueMicrotask(callback);

The function does two things:

  • it adds callback to the microtask queue;
  • it guarantees that the callback runs after the current function finishes, but before any setTimeout, setInterval, event or other macrotask.

Under the hood the order is:

  1. The current code in the Call Stack runs to completion.
  2. Once it finishes, the Event Loop checks the microtask queue.
  3. All microtasks in the queue run in order, one after another.
  4. Only then does it take the next macrotask (a setTimeout callback, for example).

Ordering next to a timer

javascript
setTimeout(() => console.log('timeout'), 0); queueMicrotask(() => console.log('microtask')); console.log('sync'); // Output: // sync // microtask // timeout

The order comes from three levels:

  1. sync is synchronous code, so it runs first;
  2. microtask is the microtask queue, which is drained immediately after the synchronous code;
  3. timeout is the macrotask queue, which the Event Loop reaches last.

Even setTimeout(fn, 0) will never beat a microtask: a zero delay only means "queue this macrotask as soon as possible", not "run it immediately".

Comparison with Promise.then

queueMicrotask() works exactly like Promise.resolve().then(callback), but without creating a Promise object, which makes it slightly faster and simpler.

javascript
Promise.resolve().then(() => console.log('microtask via Promise')); queueMicrotask(() => console.log('microtask via queueMicrotask'));

Both callbacks land in the microtask queue and run before any timers. The only difference is the cost: the promise version additionally allocates a Promise object and a then chain, while queueMicrotask() simply puts the function into the queue. If you do not need a result and are not building a chain, queueMicrotask() states the intent more precisely.

Nested microtasks and the risk of blocking the Event Loop

All microtasks run back to back. If you create a new microtask from inside a microtask, it joins the same queue and runs before control moves to a macrotask:

javascript
queueMicrotask(() => { console.log('A'); queueMicrotask(() => console.log('B')); }); // Output: // A // B

That leads straight to the danger: the microtask queue can be made endless.

javascript
function loop() { queueMicrotask(loop); } loop();

This code will hang the Event Loop: microtasks keep running forever, and no macrotask (setTimeout, clicks, rendering, network callbacks) ever gets executed. In a browser it effectively freezes the tab.

Summary

PropertyValue
What it doesAdds a function to the microtask queue
When it runsAfter the current code, but before any macrotask
EquivalentPromise.resolve().then(fn)
QueueMicrotask Queue
Returnsundefined
Use caseA light, fast alternative to promises for microtasks
StandardECMAScript 2019, all modern browsers and Node.js

Common mistakes

  • Confusing it with setTimeout(fn, 0). A timer is a macrotask, so it runs after every microtask, even with a zero delay.
  • Assuming the microtask runs "immediately". It still waits for the current function to finish completely and for the stack to empty.
  • Queueing microtasks recursively. That is not "splitting work into chunks", it is a guaranteed Event Loop block, because the microtask queue never yields to macrotasks.
  • Using it for heavy computation. A microtask blocks rendering exactly like synchronous code. To split heavy work you need macrotasks (setTimeout) or a separate Worker.
  • Expecting a return value. queueMicrotask() always returns undefined, there is no chain like in a promise and no way to read the callback's result.
  • Relying on it for error handling. An exception inside the callback does not reach any catch, it surfaces as an unhandled error, so it has to be handled inside the function itself.

Short Answer

Interview ready
Premium

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