Microtasks in depth
Predict the output of promise jobs, queueMicrotask callbacks, async await continuations, timers, and nested microtasks with real browser puzzle runs.
- 01Read microtask producersSpot promise jobs, queueMicrotask callbacks, and await resumptions.
- 02Predict output orderTrace sync code, microtasks, nested microtasks, and timer tasks.
- 03Avoid microtask trapsUse microtasks for tiny follow-ups without starving the next task.
Microtasks are the event loop’s sticky notes
The previous lesson introduced the event loop: JavaScript runs one task at a time, and when the call stack is empty the host can pick the next task. This lesson zooms into the moment between tasks: the microtask checkpoint.
A microtask is tiny work that must run after the current synchronous code finishes but before the next task, such as a timer callback. Promise callbacks, queueMicrotask, and the continuation after await all use this timing.
Imagine a meeting ends. Before you leave your desk for the next meeting, you handle every sticky note already on the desk. If one sticky note creates another urgent sticky note, you handle that too. Only when the desk is clear do you walk to the next meeting.
- In real life: Your current meeting
- In JavaScript: The current task or script
- In real life: Sticky notes on your desk
- In JavaScript: Microtasks waiting to run
- In real life: A note that says call Ada
- In JavaScript: A promise job: call this
.thencallback - In real life: Leaving for the next meeting
- In JavaScript: Taking the next task, like a timeout
Where the analogy stops: The browser is not literally using sticky notes, and rendering can involve host-specific details. The analogy is for ordering: finish the current task, clear the notes, then move on.
After the current stack clears, JavaScript drains the microtask queue in order. It keeps draining if microtasks add more microtasks. Only then can the host move to the next task.
We will make only the promises-and-await claims needed for ordering. Full promise mechanics and async functions get their own lessons next: here, a settled promise queues a .then callback as a microtask, and await x pauses the async function so the rest resumes in a later microtask.
Promise jobs: callbacks from settled promises
PROMISE JOBSA promise job is a microtask created by promise machinery. In the code below, Promise.resolve() is already fulfilled, but the .then callback still does not run immediately. JavaScript queues a job that says, “call this callback with the result.”
console.log("sync A");Promise.resolve().then(() => console.log("promise job"));console.log("sync B");// sync A, sync B, promise jobThat ordering is the first trick in almost every puzzle: the callback is ready, but it waits until the current script finishes. This is why a promise callback beats a timer but loses to ordinary synchronous lines.
A settled promise does not interrupt the line you are reading. It leaves a note: when the current work is done, call this callback with the value.
- In real life: A note says, “call Sam with the answer”
- In JavaScript: A
.thenreaction says, “call this callback with the value” - In real life: You finish your sentence first
- In JavaScript: The current synchronous code runs to completion
- In real life: Then you call Sam
- In JavaScript: The promise job runs at the microtask checkpoint
Where the analogy stops: A promise also stores state and values. We are using only the ordering piece here; promise internals come later.
queueMicrotask: write your own sticky note
INTERACTIVEqueueMicrotask(fn) is the direct API: put fn in the microtask queue. It is useful when you want to finish the current call first, then do a tiny follow-up before timers, user events, or rendering get another chance.
| Producer | What gets queued | Remember |
|---|---|---|
Promise.resolve().then(fn) | A promise reaction job | The callback runs after the current stack clears. |
queueMicrotask(fn) | Your callback as a microtask | Direct and explicit; errors behave like uncaught exceptions. |
await value | The rest of the async function | The function pauses, then resumes in a later microtask. |
| A microtask queuing another microtask | Another item at the back of the same queue | It still runs before the next task. |
setTimeout(fn, 0) | A task, not a microtask | It waits until the microtask queue is empty. |
Promise jobs and queueMicrotask callbacks share one FIFO queue. “FIFO” means first in, first out: if the promise job is queued first, it runs first; if the direct microtask is queued first, it runs first.
console.log("task start");setTimeout(() => console.log("timeout"), 0);queueMicrotask(() => { console.log("micro 1"); queueMicrotask(() => console.log("micro 2"));});Promise.resolve().then(() => console.log("promise"));console.log("task end");Run it and watch the nested micro 2 line. If microtasks were only one pass, the timeout would appear first — but it does not.
queueMicrotask, Promise.resolve().then, and setTimeout in your browser.Promise.resolve() .then(() => { console.log("then A"); throw new Error("boom"); }) .catch(() => console.log("caught rejection"));queueMicrotask(() => console.log("queued microtask"));console.log("sync");Click to run the promise side safely. A throw inside queueMicrotask would be reported like an uncaught exception, so this lesson explains it without intentionally crashing the page.
A throw inside a .then callback rejects the promise, so a later .catch can handle it. A throw inside a queueMicrotask callback is reported like an uncaught exception by the host. The demo runs the promise side because we do not intentionally crash the lesson page.
await ordering: the bookmark
AWAITAn async function starts synchronously. When it reaches await x, it places a bookmark, steps away, and the rest of the function continues in a later microtask. Modern engines have optimized this path, especially for native promises, so avoid counting exact “ticks.” Predict the output order from real runs, not folklore.
You can read a page up to a bookmark, answer the door, then come back and continue from exactly that place. await works like that for an async function.
- In real life: Read until the bookmark
- In JavaScript: Run the async function until
await - In real life: Close the book for a moment
- In JavaScript: Return control to the current script
- In real life: Resume from the bookmark
- In JavaScript: Continue the async function in a microtask
Where the analogy stops: The analogy does not describe promise assimilation or engine internals. It only explains why code after await runs later.
async function demo() { console.log("before await"); await null; console.log("after await");}demo();console.log("outside");// before await, outside, after awaitThe puzzle gallery has three await puzzles: a basic pause, two async functions interleaving, and a thenable object. A thenable is an object with a then method. Awaiting one adds promise assimilation behavior, so we state only the order proven by the real run in this lesson.
Output-order puzzle gallery
7 PUZZLESNow do the main skill: predict the next printed line, step by step. Each puzzle then runs for real in your browser and shows the captured order beside your prediction. The lesson tests also run the displayed source with Node 22’s async snippet runner.
console.log("script");Promise.resolve().then(() => console.log("promise"));queueMicrotask(() => console.log("queueMicrotask"));setTimeout(() => console.log("timeout"), 0);console.log("end");Pick the next printed line. Synchronous logs happen now; promise jobs, queueMicrotask callbacks, and await continuations wait for the microtask checkpoint; timers are later tasks.
- Left:
console.log('sync')/ Right: a.thencallback queued above it - Left: a
.thencallback / Right: asetTimeout(..., 0)callback - Left:
setTimeout(..., 0)/ Right: a microtask queued from another microtask - Left: the first
.thenqueued / Right: a laterqueueMicrotaskcallback - Left: continuation after
await null/ Right: a.thenqueued after the async call returns - Left: an async function resumes after awaiting a thenable / Right: a later
.thencallback
For each pair, decide whether the left or right item runs first in this lesson’s ordering model.
Replay one puzzle as a queue model
STEP THROUGHThe player below is a labeled model of the queues for one medium puzzle. It is not an engine debugger. It uses the real final order, then shows the important queue events: schedule a promise job, schedule a direct microtask, schedule a timer task, drain microtasks, then run the timer.
This is a labeled model of the queues for the medium puzzle. It is recorded from the lesson’s real expected order, but it is not an engine debugger.
script
Promise.resolve().then(() => { console.log("B"); Promise.resolve().then(() => console.log("D"));});queueMicrotask(() => console.log("C"));setTimeout(() => console.log("E"), 0);console.log("F");Where you’ll use microtasks
Most days you do not call queueMicrotask directly. You meet microtasks because promises and async functions are everywhere: data loading, animation setup, UI state updates, and test assertions. Knowing the order prevents “why did this log first?” bugs.
let queued = false;const pending = []; export function scheduleMessage(message) { pending.push(message); if (queued) return; queued = true; queueMicrotask(() => { queued = false; console.log(pending.join(", ")); pending.length = 0; });}This pattern lets several synchronous calls collect work, then flushes once before the next task. Keep the callback short. If the work might take noticeable time, split it into tasks instead so the page can respond.
Common misconceptions
“A resolved promise runs its then callback immediately.”
No. The callback is queued as a microtask after the current stack clears.
“setTimeout(fn, 0) means right now.”
It means a future task. Microtasks queued by the current task run first.
“The browser runs only the microtasks that were queued at first.”
It keeps draining. A microtask queued by a microtask can still run before the next task.
“queueMicrotask and Promise.resolve().then are identical.”
They share ordering, but thrown errors are reported differently.
“await always costs a fixed number of ticks.”
Modern engines changed and optimized await behavior. Trust real ordering tests, not old tick-count rules.
Practice: trace the sticky notes
5 EXERCISESPredict the output order, then type the three words in order.
console.log("one");
Promise.resolve().then(() => console.log("two"));
console.log("three");The output is one, three, two. The promise job waits until the current script finishes.
Predict why a microtask beats a 0ms timeout.
setTimeout(() => console.log("timeout"), 0);
queueMicrotask(() => console.log("micro"));
console.log("sync");The output is sync, micro, timeout. The synchronous log runs first, then the microtask, then the timer task.
Trace the bookmark: what prints before and after the pause?
async function label() {
console.log("A");
await null;
console.log("B");
}
label();
console.log("C");The output is A, C, B: the function starts, pauses at await, outside code runs, then the continuation runs as a microtask.
Read the program and type what the timeout prints.
const steps = [];
steps.push("start");
queueMicrotask(() => steps.push("microtask"));
steps.push("end");
setTimeout(() => console.log(steps.join(" -> ")), 0);const steps = [];
steps.push("start");
queueMicrotask(() => steps.push("microtask"));
steps.push("end");
setTimeout(() => console.log(steps.join(" -> ")), 0);The timeout prints start -> end -> microtask. queueMicrotask schedules the middle update after sync code but before the timeout.
Explain the bug, then compare with the solution. This one is not auto-checked because the safest answer is a design change: stop or yield.
function riskyLoop() {
queueMicrotask(riskyLoop);
}
riskyLoop();let count = 0;
function saferLoop() {
count += 1;
if (count < 3) setTimeout(saferLoop, 0);
}
saferLoop();
setTimeout(() => console.log("paint can happen"), 0);Using a task boundary gives the browser a chance to handle rendering and input between chunks. Real code should also have a stopping condition or process a bounded batch.
Quiz: check your understanding
7 QUESTIONSQuestion 1 of 7What is a promise job in this lesson?
Choose an answer to see the explanation.
Question 2 of 7What does the promise and queueMicrotask snippet print?
Read the code, then predictconsole.log("A"); Promise.resolve().then(() => console.log("B")); queueMicrotask(() => console.log("C")); console.log("D");Choose an answer to see the explanation.
Question 3 of 7Which statement about
queueMicrotaskis most accurate?Choose an answer to see the explanation.
Question 4 of 7What does the basic await snippet print?
Read the code, then predictasync function demo() { console.log("start"); await null; console.log("after"); } demo(); console.log("end");Choose an answer to see the explanation.
Question 5 of 7A microtask queues another microtask, while a 0ms timeout is waiting. What happens?
Choose an answer to see the explanation.
Question 6 of 7What does the promise error snippet print?
Read the code, then predictPromise.resolve() .then(() => { console.log("then"); throw new Error("boom"); }) .catch(() => console.log("catch")); queueMicrotask(() => console.log("micro")); console.log("sync");Choose an answer to see the explanation.
Question 7 of 7Why can an endless chain of microtasks be a problem?
Choose an answer to see the explanation.
Key takeaways
- Promise
.thencallbacks run as microtasks when the promise settles. queueMicrotaskqueues a microtask directly; keep it tiny.awaitruns code before the await now and resumes after it in a later microtask.- Microtasks drain before the next task, including microtasks queued by other microtasks.
- Too many self-queuing microtasks can starve timers, input, and rendering.
One-liner.
A microtask is a sticky note JavaScript handles after the current stack clears and before the next task begins.
Up next: Callbacks.