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
console.log('1');
Promise.resolve().then(() => console.log('2'));
console.log('3');The result:
1
3
2Why it works like that:
- First all the synchronous code runs (on the call stack).
- 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 aPromise; - 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:
- It runs all the synchronous code (the call stack).
- It runs all the microtasks, until the queue is empty.
- It moves on to one macrotask (a
setTimeout, for instance). - It again runs all the microtasks that piled up after it.
- And round it goes.
Compared with macrotasks:
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');Output:
A
D
C
BWhy:
AandDare synchronous;Cis a microtask (from the promise);Bis a macrotask (fromsetTimeout).
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:
Promise.resolve().then(() => console.log('1'));
Promise.resolve().then(() => console.log('2'));
Promise.resolve().then(() => console.log('3'));
console.log('4');Output:
4
1
2
3All 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:
Promise.resolve().then(() => {
console.log('A');
Promise.resolve().then(() => console.log('B'));
});Output:
A
BAfter 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 type | Examples | When it runs |
|---|---|---|
| Microtasks | .then(), .catch(), .finally(), queueMicrotask() | After the current code, before the next macrotask |
| Macrotasks | setTimeout, setInterval, fetch, DOM events | After 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 settledPromise.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.nextTickhas its own higher priority queue, drained before the promise queue. - Forgetting that
awaitis a microtask too. The code afterawaitdoes not resume instantly, even when you are awaiting an already available value.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.