Microtask in JavaScript: the priority queue behind promises

A microtask runs before the event loop's next task: what creates one, the order you can rely on, and how the loop ends up starved.
3 min read
Believemy logo

Two callbacks registered one after the other do not necessarily run in that order. A promise callback goes ahead of a timer callback, even when written after it.

The reason is a separate queue, served first by the event loop, whose existence explains most of the surprising execution orders in JavaScript.

The two queues of the loop and the order they are served inThe three microtasks in the queue run one after the other, then and only then is the first task picked up.Every microtask runs before the first taskTime runs to the rightMicrotask queuedrained in fullqueueMicrotaskthenawaitThe whole queue runs first, uninterruptedTask queueone per loop turnsetTimeoutThe task starts only once the queue is emptyA microtask that queues another runs in the same turn


Definition

A microtask is a short piece of work placed in a queue that is drained in full the moment the call stack empties, before any ordinary task is picked up. Three things produce one.

  • A then(), a catch or a finally on a promise that is already settled or has just settled.
  • The resumption after an await, which is the very same mechanism written differently.
  • A call to queueMicrotask, the explicit way to register one.
JAVASCRIPT
setTimeout(() => console.log("task"), 0);
queueMicrotask(() => console.log("microtask"));
Promise.resolve().then(() => console.log("promise"));
console.log("synchronous");

// synchronous
// microtask
// promise
// task


Microtask and task: the difference that matters

CriterionMicrotaskTask
OriginPromises, queueMicrotaskTimers, events, requests
Count per passThe whole queue, uninterruptedOne only, then the loop comes round
Page renderingNone until the queue is emptyPossible between two tasks

Those three rows explain the behavior you observe: the microtask queue is drained as one block, and a microtask that registers another one lengthens the same pass instead of waiting for the next.


Starving the loop

That priority comes at a price. A microtask registering itself with no stopping condition occupies the queue forever: timers stop firing, clicks stop being handled, and the page freezes without a single infinite loop showing in the code.

JAVASCRIPT
// Do not run this: the event loop never gets control back
// function loop() { queueMicrotask(loop); }
// loop();

// The correct version, which lets the loop breathe
function loopProperly(left) {
  if (left === 0) return console.log("done");
  setTimeout(() => loopProperly(left - 1), 0);
}

loopProperly(3);
Good to know

A recursion written with await has the same defect when nothing hands control back: every resumption is a microtask. Slipping in a real wait, even of one millisecond, is enough to go through the task queue again and unblock the interface.


Frequently asked questions

Question

Why prefer queueMicrotask over setTimeout(fn, 0)?

Because a microtask runs before any rendering and before any other task, which guarantees no intermediate state is ever visible. setTimeout, by contrast, lets the page repaint in between, which is sometimes exactly what you are after.


Question

Is the order between two microtasks guaranteed?

Yes, the queue is strictly first in, first out. Two then calls placed on already-fulfilled promises run in the order they were registered. That guarantee is what makes promise behavior reproducible from one run to the next.


Question

Do you need this detail to write correct code?

Not day to day, but it becomes indispensable the moment an unexpected order shows up, in tests above all: an assertion written right after an asynchronous call runs before that call resumes. Understanding the queue then explains the gap in seconds.

Related terms

Discover our javaScript glossary

Every word of JavaScript explained simply: keywords, built-in objects, methods, errors and concepts. Clear definitions and examples that actually run, to learn and to troubleshoot.

Share this article

Want to help us? Share this article on your networks or even better: on your site, in an article or in your newsletter.