Microtasks: why promise callbacks run before timers
Promise callbacks are microtasks. They all run right after the current task, before any timer — which explains most event-loop interview puzzles.
Comes up in: JavaScript, Recruiter screen, Coding
Learn#
Priya’s interviewer shows four lines with setTimeout and Promise.resolve().then, and asks for the order. Priya says the timer runs first, “because it was written first”. The interviewer smiles. It does not.
Next to the task queue there is a second line: the microtask queue. A microtask is a small callback that must run as soon as the current code ends.
Every .then callback is a microtask. The language spec calls them jobs.
The rule: when the call stack becomes empty, the browser runs a microtask checkpoint. It runs every microtask in the queue — including ones added during the checkpoint — before the next task, timer or paint.
queueMicrotask(fn) adds a microtask directly, without a promise.
console.log("A script");
setTimeout(() => console.log("E timer"), 0);
Promise.resolve().then(() => {
console.log("C promise");
queueMicrotask(() => console.log("D microtask"));
});
new Promise((resolve) => { console.log("B executor"); resolve(); });Output (verified)
A script B executor C promise D microtask E timer
Notice B: the function you pass to new Promise runs immediately. Only the reactions wait.
Call stack
- script
Microtasks
empty
Task queue
- E timer
Console
- A script
The script prints A and hands a timer to the host. When it fires, E will wait in the task queue.
Read the steps as text
- The script prints A and hands a timer to the host. When it fires, E will wait in the task queue.
- The promise is already resolved, so its callback C goes straight into the microtask queue.
- The promise executor runs right now, inside the script. B prints.
- The script ends. Before any task, every microtask runs. C prints and queues D.
- D was added during the checkpoint, so it runs in the same checkpoint.
- The microtask queue is empty. Now the next task, the timer, can run.
Predict#
Predict first
What prints first? new Promise((r) => { console.log('X'); r(); }).then(() => console.log('Z')); console.log('Y');
The executor — the function passed to new Promise — runs synchronously, so X prints first, then Y, then Z. “Promises are asynchronous” is only true for .then callbacks, not for the code that creates the promise.
Interview angle#
Say it like this — about 30 seconds
“After each task, when the call stack is empty, the browser runs a microtask checkpoint. Promise reactions and
queueMicrotaskcallbacks go there, and all of them run — even new ones added meanwhile — before the next task or paint. So a resolved promise’s.thenalways beatssetTimeout(fn, 0). The promise executor itself is synchronous. And if microtasks keep adding microtasks, the page starves: no timers, clicks or rendering.”
Follow-up Is the promise executor synchronous or asynchronous?
new Promise(...). Only the .then, .catch and .finally callbacks are queued as microtasks.Follow-up What happens if a microtask keeps queueing new microtasks?
Follow-up Where do MutationObserver callbacks run?
Practice#
faded
Build batch(fn) — the trick behind batched updates in UI libraries. Calls to add during one task are collected and passed to fn once, as an array, in a microtask.
Done when
add(1); add(2); add(3)in one task callsfn([1, 2, 3])once.fnruns before asetTimeout(…, 0)that was scheduled earlier.- Calls in a later task start a new batch; no calls means no
fncall.
Remember#
Try to answer each card before you open it. Then add them to your reviews.
When does the microtask queue run?
Whenever the call stack empties, before the next task, timer or paint.
Is the function passed to new Promise run now or later?
Now, synchronously. Only .then/.catch/.finally callbacks wait.
What does this print?setTimeout(() => console.log("T"), 0);
Promise.resolve().then(() => console.log("P"));
console.log("S");
setTimeout(() => console.log("T"), 0);
Promise.resolve().then(() => console.log("P"));
console.log("S");S P T
What is microtask starvation?
Microtasks keep adding microtasks, so tasks and rendering never get a turn.
Go deeper
Four layers. Stop at the one your interview needs; each ends with a quick check.
The core ideaEvery round
Two queues. After each task — when the stack is empty — all microtasks run, then the next task. Promise callbacks and queueMicrotask are microtasks; timers, clicks and network events are tasks.
Check yourself: A microtask adds another microtask. Does it run before the next timer?
Yes. The checkpoint runs until the microtask queue is empty.
How it works underneathSenior rounds
ECMA-262 defines promise jobs and leaves running them to the host Source: ECMA-262: Jobs and host operations to enqueue jobs. The HTML Standard runs them at a microtask checkpoint, which happens whenever a callback returns and the stack is empty — so two click listeners each get their microtasks run in between Source: HTML Standard: Perform a microtask checkpoint.
Check yourself: Does the checkpoint only happen at the end of a task?
No. It also happens after each callback the browser calls, whenever the stack becomes empty.
Edge cases and trapsWhere people slip
Starvation in action — the timer waits until the chain stops:
let n = 0;
setTimeout(() => console.log("timer ran after", n, "microtasks"), 0);
function again() { if (++n < 100000) queueMicrotask(again); }
queueMicrotask(again);Output (verified)
timer ran after 100000 microtasks
With no stopping condition, the page would freeze. MutationObserver callbacks are also microtasks.
Check yourself: Why does the timer below wait for all 100,000 microtasks?
The checkpoint keeps running while microtasks keep arriving; the timer is a task.
In productionStaff-level answers
Frameworks use microtasks to batch work: Vue’s nextTick and many state libraries collect changes made in one task and apply them once, before paint. Keep microtasks short. Long or endless microtask chains block input as badly as a long loop, and they do not show up as separate tasks in performance tools.
Check yourself: Why is a microtask a good place to flush batched updates?
It runs after all synchronous changes in the task but before the browser paints.
Finished the five steps? Mark it, and its cards join your reviews.
Last checked 2026-10-31. Spot a mistake? Use “Report an error” below — fixes are listed on the corrections page.
Interview questions this answers
- Is the promise executor synchronous or asynchronous?
- What happens if a microtask keeps queueing new microtasks?