queueMicrotask()
queueMicrotask(callback) The queueMicrotask() function schedules a callback to run as a microtask: a short job that fires at the end of the active task, before the event loop starts the next one or hands control back to the browser for rendering. It is the direct, allocation-free way to defer work to a microtask without going through a Promise.
Syntax
queueMicrotask(callback)
Parameters
| Parameter | Type | Description |
|---|---|---|
callback | function | The function to run as a microtask. It is called with no arguments and its return value is ignored. |
Return value
undefined. The function does not return a thenable, so you cannot await it or chain .then() on it.
Throws
TypeError synchronously if callback is not callable (for example, null, a number, or an object without a .call method). The check happens at the call site, not when the microtask runs.
Description
The event loop processes one task (also called a macrotask) at a time: a script execution, a dispatched event, a setTimeout callback, an I/O completion, and so on. After each task, the engine drains the microtask queue completely before the next job starts. Sources of microtasks include Promise.prototype.then() callbacks, the continuation after an await, and queueMicrotask().
Use queueMicrotask() when you want to defer work until the current task finishes, but you do not want to allocate a Promise to do it. Two practical cases:
- Defer cleanup that must run after the current synchronous work. Useful for batching state updates, flushing buffers, or notifying observers without yielding to a full task boundary.
- Schedule work that should fire before the next timer or render. A queued microtask runs strictly before the browser may paint the next frame and before the next
setTimeout(0)callback.
It is available on window in the main browser thread, on self inside Web Workers and Service Workers, and as a global in Node.js (since 11.0.0), Deno, and Bun.
A quick illustration of the ordering: any synchronous output logs first, the deferred work logs next, and only then does the next macrotask get a turn.
console.log("script start");
queueMicrotask(() => console.log("deferred work"));
console.log("script end");
// script start
// script end
// deferred work
Examples
Run after the current task, before any timer
Microtasks fire after the current synchronous script finishes, but before the next job on the task queue. The setTimeout callback therefore runs last:
console.log("1: sync");
setTimeout(() => {
console.log("2: timeout 0");
}, 0);
queueMicrotask(() => {
console.log("3: microtask");
});
console.log("4: sync end");
// 1: sync
// 4: sync end
// 3: microtask
// 2: timeout 0
This ordering is the most common reason to reach for queueMicrotask(). If you want a callback to fire as soon as the current call stack unwinds, microtask scheduling is the right tool. Macrotask scheduling (timers, I/O) is only appropriate when you specifically want to yield to other jobs first.
Microtasks run in FIFO order
Multiple calls during the same task are queued and run in the order they were enqueued:
queueMicrotask(() => console.log("microtask A"));
queueMicrotask(() => console.log("microtask B"));
queueMicrotask(() => console.log("microtask C"));
console.log("sync");
// sync
// microtask A
// microtask B
// microtask C
The order is well-defined and predictable across engines. That makes microtask scheduling useful for ordering work that depends on a stable, synchronous call sequence within a single task.
Nested microtasks drain in the same pass
If a microtask enqueues another microtask, the new one runs in the same drain, before the event loop starts the next job. This is the rule that makes await work and is also the rule that creates the starvation risk covered in the gotchas:
queueMicrotask(() => {
console.log("first");
queueMicrotask(() => console.log("nested"));
});
queueMicrotask(() => console.log("second"));
// first
// second
// nested
The first two run because they were queued before the drain started. The nested callback is scheduled during the drain itself and still completes in that same pass, ahead of any macrotask.
Equivalent to Promise.resolve().then(), with one important difference
Both forms schedule a microtask, but they differ in how thrown errors are reported:
// Both queue a microtask:
queueMicrotask(() => doCleanup());
Promise.resolve().then(() => doCleanup());
If the callback throws:
queueMicrotask()reports the error as a normal uncaught exception, the same way a synchronous throw would. The stack trace points at the throw site.Promise.resolve().then()produces an unhandled promise rejection, which the runtime may report separately (and which crashes Node.js processes by default).
Prefer queueMicrotask() when you do not want the extra Promise allocation and when you want a thrown error to surface as a regular exception rather than a silent rejection.
Common pitfalls
The argument must be callable
queueMicrotask(); // TypeError: Argument 1 is not a function
queueMicrotask(null); // TypeError
queueMicrotask("log"); // TypeError
queueMicrotask({ run() {} }); // TypeError (no .call method on the object itself)
The check happens at the call site. If you need to pass arguments, wrap the work in a function and capture them via closure, because the callback receives no arguments.
Microtask starvation
A microtask can enqueue another microtask, and the engine will keep draining until the queue is empty. If you keep scheduling microtasks unconditionally from inside a microtask, the event loop never gets a chance to start the next job, paint, or process I/O. Bound the recursion explicitly:
let pending = items.length;
function processOne() {
doWork(items[items.length - pending]);
pending -= 1;
if (pending > 0) {
queueMicrotask(processOne);
}
}
queueMicrotask(processOne);
process.nextTick is not the same thing
In Node.js, process.nextTick(fn) schedules a callback that runs before microtasks, on a separate queue. Mixing the two in code that is meant to run in both Node and the browser produces different ordering across runtimes. Stick to queueMicrotask() for cross-runtime code and reserve process.nextTick for Node-only paths.
setTimeout(..., 0) is not equivalent
setTimeout schedules a macrotask. A queued timer callback waits for the next event-loop iteration, after pending microtasks and any I/O that has completed. If you need “after the current task, not after the next task”, use queueMicrotask(). If you need “after the browser can paint” or “after I/O”, use setTimeout (or requestAnimationFrame for paint-bound work).
Browser and runtime support
Available across browsers since July 2020 (Chrome 71, Edge 79, Firefox 69, Safari 12.1). Available in Node.js 11.0.0 and later, in Deno, in Bun, and in Web Workers and Service Workers via self.queueMicrotask(). No polyfill is required for modern targets; for older environments, the npm queue-microtask package is the standard shim.
See also
- Promise.prototype.then() — schedule a microtask via a Promise chain
- Promise.resolve() — turn a value into a resolved Promise
- The JavaScript event loop — tasks, microtasks, and execution ordering