Skip to main content

The promise microtask

A microtask is a small task that runs after the current call stack but before the engine moves on to macrotasks (setTimeout, setInterval, DOM events). Promise handlers always land on the microtask queue, which is exactly why they run ahead of timers.

Theory

TL;DR

  • Microtasks are the queue of urgent work, drained right after the current synchronous code.
  • .then(), .catch() and .finally() are never called synchronously: they are queued as microtasks.
  • After every macrotask the engine runs all microtasks, and only then takes the next macrotask.
  • Microtasks run in the order they were added, and a microtask created while the queue is being drained runs in the same pass.
  • That is why a promise callback always beats setTimeout(..., 0).

Quick example

javascript
console.log('1'); Promise.resolve().then(() => console.log('2')); console.log('3');

The result:

javascript
1 3 2

Why it works like that:

  1. First all the synchronous code runs (on the call stack).
  2. Then the event loop sees the stack is empty and runs all the microtasks, in this case the .then() callback.

Promises create microtasks

When a promise settles as fulfilled or rejected, its .then(), .catch() and .finally() handlers do not run immediately, they are placed on the microtask queue.

Microtasks are created:

  • by .then(), .catch(), .finally() on a Promise;
  • by queueMicrotask(fn);
  • by MutationObserver (in the browser);
  • by process.nextTick() in Node.js.

Every await inside an async function also schedules a microtask: resuming the function after await is that same promise microtask.

The microtask queue and the macrotask queue

The JavaScript engine processes events like this:

  1. It runs all the synchronous code (the call stack).
  2. It runs all the microtasks, until the queue is empty.
  3. It moves on to one macrotask (a setTimeout, for instance).
  4. It again runs all the microtasks that piled up after it.
  5. And round it goes.

Compared with macrotasks:

javascript
console.log('A'); setTimeout(() => console.log('B'), 0); Promise.resolve().then(() => console.log('C')); console.log('D');

Output:

javascript
A D C B

Why:

  1. A and D are synchronous;
  2. C is a microtask (from the promise);
  3. B is a macrotask (from setTimeout).

The microtask C runs before the timer B.

The important rule: after every macrotask the engine always drains the microtask queue completely before taking the next macrotask.

A simple analogy: the event loop is a queue in a coffee shop. Macrotasks are the ordinary large orders, microtasks are the quick change and settling up. The barista makes an order (synchronous code), then hands out all the change (microtasks), and only then moves on to the next customer (a new macrotask).

Ordering inside the queue

Several microtasks run strictly in the order they were added:

javascript
Promise.resolve().then(() => console.log('1')); Promise.resolve().then(() => console.log('2')); Promise.resolve().then(() => console.log('3')); console.log('4');

Output:

javascript
4 1 2 3

All the .then() callbacks land on one microtask queue and run in the order they were added, once the synchronous code (4) has finished.

A microtask created inside another microtask also runs in the same pass:

javascript
Promise.resolve().then(() => { console.log('A'); Promise.resolve().then(() => console.log('B')); });

Output:

javascript
A B

After A the new microtask B runs, the one added while the previous handler was executing. The engine does not leave the queue until it is completely empty.

Summary

Task typeExamplesWhen it runs
Microtasks.then(), .catch(), .finally(), queueMicrotask()After the current code, before the next macrotask
MacrotaskssetTimeout, setInterval, fetch, DOM eventsAfter all the microtasks of the current tick

Promises always use microtasks, which is why their callbacks run earlier than timers.

Common mistakes

  • Assuming .then() runs synchronously. Even on an already settled Promise.resolve() the callback is queued and fires after the rest of the synchronous code.
  • Expecting setTimeout(..., 0) to fire straight away. It is a macrotask, it waits until the microtask queue is empty, so any .then() gets there first.
  • Feeding the microtask queue endlessly. If each microtask adds another one, the queue never empties: rendering and timers never get their turn, and the page freezes.
  • Confusing process.nextTick() with promise microtasks in Node.js. nextTick has its own higher priority queue, drained before the promise queue.
  • Forgetting that await is a microtask too. The code after await does not resume instantly, even when you are awaiting an already available value.

Short Answer

Interview ready
Premium

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