Microtasks in depth
Predict the output of promise jobs, queueMicrotask callbacks, async await continuations, timers, and nested microtasks with real browser puzzle runs.
- 01Spot microtasks in codeFind the three things that add microtasks:
.thencallbacks,queueMicrotask, and code afterawait. - 02Predict the printed orderOrder ordinary code, microtasks, microtasks added by microtasks, and timer tasks.
- 03Use microtasks safelyUse a microtask for a small follow-up job, without making the next task wait forever.
Microtasks: small jobs that wait for your code to finish
In the last lesson you met the event loop. Here is a short reminder:
- A task is one piece of work, like running your script or running a timer callback.
- JavaScript runs one task at a time, from start to end.
- The call stack is the list of functions that are running right now. When a task ends, the call stack is empty.
This lesson is about the short gap between one task and the next.
A microtask is a small job that waits for the current code to finish. It runs after the current task, but before the next task, such as a timer callback. Three things make microtasks:
- the callback you pass to
.thenon a promise, - the callback you pass to
queueMicrotask, - the rest of an async function after
await.
Your meeting ends. Before you walk to the next meeting, you deal with every sticky note on your desk. If a note makes you write a new note, you deal with that one too. You leave only when the desk is clear.
- In real life: Your current meeting
- In JavaScript: The current task, like your 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: Walking to the next meeting
- In JavaScript: Starting the next task, like a timer callback
When the current task ends and the call stack is empty, JavaScript runs every waiting microtask, in the order they were added. If a microtask adds a new microtask, that one runs too. The next task can start only when no microtasks are left. This moment between tasks is called the microtask checkpoint.
This lesson uses only a few facts about promises and await. The next lessons cover them in full. For now you need two facts:
- When a promise is settled (finished), its
.thencallback is added as a microtask. await xpauses an async function. The rest of the function runs later, as a microtask.
Promise jobs: a .then callback waits its turn
PROMISE JOBSA promise is an object that will hold a result. A promise job is a microtask that calls a .then callback with that result. In the code below, Promise.resolve() makes a promise that already has its result. Even so, the .then callback does not run right away. JavaScript adds a job to the microtask queue 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 jobIn plain words, line by line:
- Line 1 prints
sync Aright away. - Line 2 adds the
.thencallback to the microtask queue. Nothing prints yet. - Line 3 prints
sync Bright away. - The script ends. Now the microtask runs and prints
promise job.
.then callback on line 2 waits as a microtask, so it prints last.console.log("start");Promise.resolve().then(() => console.log("first reaction"));Promise.resolve().then(() => console.log("second reaction"));console.log("end");// start, end, first reaction, second reactionLines 2 and 3 each add one .then callback to the microtask queue. Lines 1 and 4 are ordinary code, so they print first: start, then end. Then the two callbacks run in the order they were added. So a .then callback runs after the ordinary lines, but before a timer callback.
A finished promise does not stop the line that is running. It leaves a note instead: when the current work is done, call this callback with the result.
- In real life: A note says: call Sam with the answer
- In JavaScript: A promise job says: call this
.thencallback with the result - In real life: You finish your sentence first
- In JavaScript: The current code runs to the end
- In real life: Then you call Sam
- In JavaScript: The promise job runs at the microtask checkpoint
queueMicrotask: add your own microtask
INTERACTIVEqueueMicrotask(fn) adds the function fn to the microtask queue. You call it directly, without a promise. Use it when you want a small follow-up job to run:
- after the current code finishes,
- but before timers, clicks, or the next screen update get a turn.
The table below lists the common ways to make a microtask, plus one way that is not a microtask. Read each row as: "when I write this, this gets added, and this is what to remember."
| When you write | What gets added | What to remember |
|---|---|---|
Promise.resolve().then(fn) | A promise job that calls fn. | fn runs after the current code finishes. |
queueMicrotask(fn) | fn itself, as a microtask. | You add it directly. If fn throws, the error is uncaught. |
await value | The rest of the async function. | The function pauses, then continues in a later microtask. |
| A microtask that adds another microtask | A new microtask 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 wait in the same queue. This queue is FIFO, which means "first in, first out", like a line at a shop. Whatever was added first runs first.
A microtask can also add another microtask. The new one goes to the back of the same queue, and it still runs before the next task. The experiment below shows this with a timer that is already waiting.
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 the code and watch for micro 2. If JavaScript ran only the microtasks that were waiting at the start, timeout would print before micro 2.
queueMicrotask, Promise.resolve().then, and setTimeout calls in your browser.micro 1 adds micro 2 to the back of the same queue. The timer runs only when the microtask queue is empty.Next, see what happens when a .then callback throws an error.
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 code. An error inside a queueMicrotask callback would be an uncaught error, so this lesson only describes that case instead of running it.
Some browser callbacks look alike but run at different times. A MutationObserver watches part of the page, called the DOM, and calls you back when it changes. Read each row of the table as: "this callback waits here, and this is when it runs."
| Callback | Where it waits | When it runs |
|---|---|---|
MutationObserver | The microtask queue. | It runs after the page changes, before the next task. |
setTimeout | The task queue. | It runs later, after the microtask checkpoint. |
requestAnimationFrame | A list of jobs for the next screen update. | It runs just before the browser draws the page. It is not a microtask. |
const box = document.createElement("div");const observer = new MutationObserver(() => console.log("mutation"));observer.observe(box, { childList: true });box.append("done");console.log("sync");// sync, mutationIn plain words, line by line:
- Line 1 makes an empty
divcalledbox. - Line 2 makes an observer whose callback prints
mutation. - Line 3 tells the observer to watch
boxfor new children. - Line 4 adds the text
doneto the box. The callback does not run yet. - Line 5 prints
sync.
The browser reports the change at the microtask checkpoint, so mutation prints after sync. This is one reason microtasks matter outside promise code.
If a .then callback throws an error, its promise becomes rejected (failed). A later .catch can then handle the error. If a queueMicrotask callback throws, there is no promise, so no .catch can handle it. The browser reports it as an uncaught error. The experiment above runs only the promise side, so it doesn't break this page.
await: pause now, continue later
AWAITAn async function is a function written with the async keyword. Inside it you can use await. When you call one, this happens:
- The function starts right away, like any other function.
- When it reaches
await x, it pauses. - The code that called it keeps running.
- Later, the rest of the function runs as a microtask.
JavaScript engines have changed how many inner steps await takes, so don't try to count them. Predict the order from real runs, like the ones in this lesson.
You read a book up to your bookmark. The doorbell rings, so you close the book and answer the door. Then you come back and keep reading from the bookmark. await does the same 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: Let the rest of the script run
- In real life: Open the book at the bookmark
- In JavaScript: Continue the async function in a microtask
async function demo() { console.log("before await"); await null; console.log("after await");}demo();console.log("outside");// before await, outside, after awaitIn plain words, in the order things happen:
- Line 6 calls
demo(). - Line 2 prints
before awaitright away. - Line 3 pauses
demo. - Line 7 prints
outside. - The script ends. The rest of
demoruns as a microtask: line 4 printsafter await.
demo() runs until await null and pauses. The script prints outside, then the rest of demo runs as a microtask.async function readValue() { console.log("before"); const value = await 3; console.log("value", value);}readValue();console.log("outside");// before, outside, value 3await does not need a promise. Line 3 waits for the number 3. JavaScript treats 3 like a promise that already has its result. The rest of the function still waits for a microtask. So before and outside print first, then value 3.
The puzzle gallery below has three await puzzles:
- a basic pause,
- two async functions that take turns,
- a thenable: a plain object that has its own
thenmethod.
Awaiting a thenable adds an extra step. JavaScript first calls the object's then method in a microtask. For that puzzle, this lesson states only the order that the real run shows.
Puzzles: what prints first?
7 PUZZLESNow practice the main skill: predict the order of the printed lines. For each puzzle, pick the lines one by one, in the order you think they print. Then run the puzzle for real in your browser and compare. The lesson tests also run every puzzle in Node 22, so every answer is checked.
console.log("script");Promise.resolve().then(() => console.log("promise"));queueMicrotask(() => console.log("queueMicrotask"));setTimeout(() => console.log("timeout"), 0);console.log("end");Pick the line you think prints next. Ordinary code prints now. .then callbacks, queueMicrotask callbacks, and code after await wait for the microtask checkpoint. Timers wait for a later task.
- Left:
console.log('sync')/ Right: a.thencallback added on the line above it - Left: a
.thencallback / Right: asetTimeout(..., 0)callback - Left:
setTimeout(..., 0)/ Right: a microtask added by another microtask - Left: the first
.thencallback added / Right: aqueueMicrotaskcallback added later - Left: the code after
await null/ Right: a.thencallback added after the async function pauses - Left: the code after awaiting a thenable / Right: a
.thencallback added later
For each pair, decide whether the left side or the right side runs first.
Step through one puzzle, queue by queue
STEP THROUGHThe player below steps through the second puzzle, the one that prints A to F. It shows what has printed, what waits in the microtask queue, and what waits as the next task. It is a teaching model, not a real debugger, but its final order comes from a real run. Each step shows one of these events:
- a promise job is added,
- a microtask is added with
queueMicrotask, - a timer task is added,
- a microtask runs,
- the timer task runs.
This is a teaching model of the queues for the A to F puzzle. Its order comes from a real run, but it is not a real 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
You won't often call queueMicrotask yourself. But promises and async functions are everywhere: loading data, updating the page, and testing. Knowing the order helps you fix bugs like "why did this print first?"
let queued = false;const pending = []; function scheduleMessage(message) { pending.push(message); if (queued) return; queued = true; queueMicrotask(() => { queued = false; console.log(pending.join(", ")); pending.length = 0; });} scheduleMessage("first");scheduleMessage("second");// first, secondThis pattern is called batching: collect several small jobs, then handle them together. In plain words, line by line:
- Line 1:
queuedremembers whether a microtask is already waiting. - Line 2:
pendingis a list that collects the messages. - Line 5 adds the message to the list.
- Line 6: if a microtask is already waiting, the function stops here.
- Lines 7 and 8: otherwise, it sets
queuedtotrueand adds one microtask. - Lines 9 to 11 run later: they reset
queued, print every message, and empty the list. - Lines 15 and 16 call the function twice. Only the first call adds a microtask.
So the output is one line: first, second.
pending. Later, one microtask prints the whole list in one line.Keep the microtask short. If the work could take a long time, split it into tasks, such as timer callbacks, so the page can still respond.
process.nextTick(() => console.log("nextTick"));Promise.resolve().then(() => console.log("promise"));queueMicrotask(() => console.log("queueMicrotask"));console.log("sync");// sync, nextTick, promise, queueMicrotaskIn Node.js, process.nextTick adds a callback to a special Node queue. That queue runs before promise jobs and queueMicrotask callbacks. So this code prints sync, nextTick, promise, queueMicrotask. Don't use it in browser code: browsers have no process object and no nextTick queue.
Common misconceptions
"A promise that already has its result runs its .then callback right away."
No. The callback is added as a microtask. It runs after the current code finishes.
"setTimeout(fn, 0) means right now."
No. It means a future task. Microtasks added by the current task run first.
"JavaScript runs only the microtasks that were waiting at the start."
No. It keeps going until the queue is empty. A microtask added by another microtask still runs before the next task.
"queueMicrotask and Promise.resolve().then are the same."
They run in the same queue, in the same order. But errors are handled differently.
"await always takes the same number of steps."
No. Engines have changed and sped up await. Trust real runs, not old step-counting rules.
"You can only await a promise."
No. You can await any value. Even after await 3, the rest of the async function runs later.
"Every place that makes microtasks works the same way."
No. Browsers and Node.js add their own details. For example, Node's process.nextTick runs before promise jobs.
Practice: predict the order
6 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 script finishes.
Predict the order. Why does the microtask print before a timer with a 0ms delay?
setTimeout(() => console.log("timeout"), 0);
queueMicrotask(() => console.log("micro"));
console.log("sync");The output is sync, micro, timeout. The ordinary log runs first, then the microtask, then the timer task.
What prints before the pause, and what prints after it?
async function label() {
console.log("A");
await null;
console.log("B");
}
label();
console.log("C");The output is A, C, B. The function starts and prints A, then pauses at await. The outside code prints C. Then the rest of the function prints B as a microtask.
Read the program and type what the timer 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 timer prints start -> end -> microtask. queueMicrotask adds the middle step after the ordinary code, but before the timer.
Explain the bug, then compare your idea with the solution. This one is not checked automatically, because the fix is a change of design: stop the loop, or give the browser a turn.
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);Each step now runs as its own task. Between tasks, the browser can update the screen and handle clicks. The count < 3 check also makes the loop stop.
Predict which line prints first when an async function awaits the number 3.
async function showPrice() {
const price = await 3;
console.log(price);
}
showPrice();
console.log("outside");The output starts with outside, then 3. await accepts a plain value, but the code after it still runs later.
Quiz: check your understanding
8 QUESTIONSQuestion 1 of 8What is a promise job?
Choose an answer to see the explanation.
Question 2 of 8What does this code with a promise and
queueMicrotaskprint?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 8Which sentence about
queueMicrotaskis true?Choose an answer to see the explanation.
Question 4 of 8What does this async function code 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 8A microtask adds another microtask while a 0ms timer is waiting. What happens?
Choose an answer to see the explanation.
Question 6 of 8What does this code with an error in
.thenprint?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 8Why is a chain of microtasks that never ends a problem?
Choose an answer to see the explanation.
Question 8 of 8What happens when an async function awaits the number
3?Choose an answer to see the explanation.
Key takeaways
- A
.thencallback runs as a microtask, after the current code finishes. queueMicrotaskadds a microtask directly. Keep that job small.awaitruns the code before it now, and the code after it later, as a microtask.- All microtasks run before the next task, even ones added by other microtasks.
- Microtasks that keep adding more microtasks can block timers, clicks, and screen updates.
One-liner.
A microtask is a small job that runs after the current code finishes and before the next task starts.
Up next: Callbacks.