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)addsfnto the Microtask Queue and returnsundefined.- 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
console.log('A');
queueMicrotask(() => console.log('B'));
console.log('C');
// Output:
// A
// C
// BFirst 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:
queueMicrotask(callback);The function does two things:
- it adds
callbackto 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:
- The current code in the Call Stack runs to completion.
- Once it finishes, the Event Loop checks the microtask queue.
- All microtasks in the queue run in order, one after another.
- Only then does it take the next macrotask (a
setTimeoutcallback, for example).
Ordering next to a timer
setTimeout(() => console.log('timeout'), 0);
queueMicrotask(() => console.log('microtask'));
console.log('sync');
// Output:
// sync
// microtask
// timeoutThe order comes from three levels:
syncis synchronous code, so it runs first;microtaskis the microtask queue, which is drained immediately after the synchronous code;timeoutis 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.
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:
queueMicrotask(() => {
console.log('A');
queueMicrotask(() => console.log('B'));
});
// Output:
// A
// BThat leads straight to the danger: the microtask queue can be made endless.
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
| Property | Value |
|---|---|
| What it does | Adds a function to the microtask queue |
| When it runs | After the current code, but before any macrotask |
| Equivalent | Promise.resolve().then(fn) |
| Queue | Microtask Queue |
| Returns | undefined |
| Use case | A light, fast alternative to promises for microtasks |
| Standard | ECMAScript 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 returnsundefined, 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 readyA concise answer to help you respond confidently on this topic during an interview.