queueMicrotask()
Schedules a callback to run at the end of the current task, before any timer or rendering.
queueMicrotask(callback)- Added in JavaScript Baseline widely available since July 2020
Parameters
| Parameter | Type | Description |
|---|---|---|
$callback | Function | A function to be executed when the engine determines it is safe to call it: after the current task and all pending microtasks ahead of it, but before control returns to the event loop. |
Return value
undefinedNothing.
Errors
Nothing at the call site for a valid function. An exception thrown inside the callback is reported to the global error handling mechanism and does not prevent later microtasks from running.
queueMicrotask() puts a function on the microtask queue. That queue is drained completely as soon as the current task and its call stack finish — before the next timer callback, before any event handler, and before the browser renders. Microtasks run in FIFO order, and microtasks queued by microtasks join the same drain rather than waiting for the next turn.
When to reach for it
The main use is making an API consistently asynchronous. A function that sometimes calls back synchronously (cache hit) and sometimes asynchronously (cache miss) is a well-known source of ordering bugs; scheduling the fast path through a microtask means callers can rely on the callback never running before the function returns. It is also the honest way to say "after the current work settles" without inventing a promise you do not need, and without the throttled, render-delayed semantics of setTimeout(fn, 0) — a timer also yields to rendering and, once it has been nested five levels deep, gets clamped to a 4 ms minimum.
What trips people up
Microtasks can starve the event loop. Because the queue must empty before anything else happens, a callback that keeps queueing another one will hang the page — no timer, no input, no paint. Prefer setTimeout when the work should yield to rendering. Also note that a throw inside the callback is reported globally rather than to your caller, so wrap risky work in try/catch yourself.
Example
When to use it
- Making a cache-hit callback async so it never fires before the function returns
- Batching several state changes and flushing them once at the end of the current task
- Running cleanup after the current call stack unwinds but before the next render
- Reasoning about why promise callbacks beat setTimeout(fn, 0) in the console
Frequently asked questions
What is the difference between queueMicrotask and setTimeout(fn, 0)?
queueMicrotask schedules a microtask that runs as soon as the current call stack empties, before the next task and before rendering, while setTimeout(fn, 0) schedules a task that runs after all pending microtasks, after any rendering, and subject to clamping and background throttling. Use the microtask for "right after this finishes" and the timer when you want to yield to the browser.
Is queueMicrotask the same as Promise.resolve().then()?
They schedule onto the same microtask queue and run in the order they were queued, so the observable ordering is identical. queueMicrotask avoids allocating a promise and states the intent directly, but a promise gives you rejection handling, whereas an exception thrown inside a queueMicrotask callback is reported to the global error handler instead.
Can queueMicrotask freeze the browser?
Yes. The event loop must drain the entire microtask queue before it can process the next task, so a callback that queues another microtask without a termination condition blocks timers, input and rendering indefinitely. Use setTimeout for recursive work that must let the browser breathe.
More examples
Making a sometimes-sync callback always async
The cache hit is available immediately, but routing it through a microtask guarantees the callback fires after get() returns. The array proves the ordering: "get() returned" is recorded first, so callers never see the callback run before their own next line.
const cache = new Map([["a", 1]]);
const order = [];
function get(key, cb) {
if (cache.has(key)) {
queueMicrotask(() => cb(cache.get(key))); // never synchronous
} else {
setTimeout(() => cb(null), 10);
}
}
get("a", (v) => order.push(`callback ran with ${v}`));
order.push("get() returned");
await new Promise((r) => setTimeout(r, 30));
console.log(order);Microtasks starve tasks
A thousand microtasks each queue the next one, and every single one runs before the zero-delay timer gets its turn. Remove the count < 1000 guard and the page hangs permanently — this is the failure mode to watch for.
let count = 0;
setTimeout(() => console.log(`timer finally ran after ${count} microtasks`), 0);
function chain() {
if (++count < 1000) queueMicrotask(chain);
}
queueMicrotask(chain);
await new Promise((r) => setTimeout(r, 50));
console.log("total microtasks:", count);Batching work into one flush
Three synchronous updates schedule only one microtask, because the scheduled flag suppresses the rest. The flush runs after the current task completes and reports all three values at once — the same coalescing trick reactive UI libraries use.
const pending = [];
let scheduled = false;
function update(value) {
pending.push(value);
if (!scheduled) {
scheduled = true;
queueMicrotask(() => {
console.log("flushed once with:", pending.join(", "));
pending.length = 0;
scheduled = false;
});
}
}
update("a"); update("b"); update("c");
console.log("three updates queued, zero flushes so far");
await new Promise((r) => setTimeout(r, 10));
Discussion