Microtask Queue
The Priority Lane That Beats setTimeout
LinkedIn Hook
Here is the interview question that separates juniors from seniors:
console.log("A"); setTimeout(() => console.log("B"), 0); Promise.resolve().then(() => console.log("C")); console.log("D");The output is A, D, C, B — NOT A, D, B, C.
Why? Because Promises,
async/await,MutationObserver, andqueueMicrotaskall feed a different queue — the Microtask Queue — and the Event Loop drains EVERY microtask before it touches a singlesetTimeout. EvensetTimeout(fn, 0)cannot cut that line.This one detail is the reason
async/awaitcontinuations feel "instant," whyPromise.thenchaining behaves the way it does, and why a runaway promise chain can freeze an entire page.In this lesson you'll see the microtask priority rule in action, walk through four levels of predict-the-output puzzles (including
async/await), and learn when to reach forqueueMicrotask().Read the full lesson -> [link]
#JavaScript #Promises #Microtasks #AsyncAwait #EventLoop #InterviewPrep #AsyncJS
What You'll Learn
- What goes into the Microtask Queue and why it always beats the Callback Queue
- How
async/awaitcontinuations become microtasks under the hood - Why
queueMicrotask()exists and when to reach for it
The Priority Lane
The Microtask Queue is a higher-priority queue that gets drained completely before the Event Loop picks the next macrotask. This is where Promise.then(), .catch(), .finally(), MutationObserver, and queueMicrotask() callbacks go.
Think of it like an emergency lane on a highway. Regular traffic (macrotasks) follows the normal lane. But ambulances (microtasks) always get priority — they pass through first. And if more ambulances keep coming, regular traffic keeps waiting.
What Goes Into the Microtask Queue?
| Source | Example |
|---|---|
Promise.then / .catch / .finally | Promise.resolve().then(cb) |
async/await | Code after await |
MutationObserver | DOM mutation callbacks |
queueMicrotask() | queueMicrotask(cb) |
The Golden Rule: Microtasks Beat Macrotasks
console.log("Start");
setTimeout(() => console.log("Timeout"), 0);
Promise.resolve().then(() => console.log("Promise"));
console.log("End");
Output:
Start
End
Promise
Timeout
Why does Promise win? After the synchronous code finishes (printing "Start" and "End"), the Event Loop checks the Microtask Queue first. The Promise .then callback is there, so it runs before the setTimeout callback in the Callback Queue.
Predict the Output — Level 2
console.log("1");
setTimeout(() => {
console.log("2");
}, 0);
Promise.resolve().then(() => {
console.log("3");
}).then(() => {
console.log("4");
});
Promise.resolve().then(() => {
console.log("5");
});
console.log("6");
Output:
1
6
3
5
4
2
Step-by-step:
"1"-> prints (synchronous)setTimeoutcallback -> Callback Queue- First
Promise.resolve().then(() => log("3"))-> Microtask Queue - The
.then(() => log("4"))is NOT queued yet — it chains after "3" resolves - Second
Promise.resolve().then(() => log("5"))-> Microtask Queue "6"-> prints (synchronous)- Call stack empty -> drain Microtask Queue:
"3"prints -> this resolves the first chain ->.then(() => log("4"))now enters Microtask Queue"5"prints (was already in queue)"4"prints (just entered queue)
- Microtask Queue empty -> pick macrotask ->
"2"prints
Predict the Output — Level 3 (The Interviewer's Favorite)
console.log("Start");
setTimeout(() => {
console.log("Timeout 1");
Promise.resolve().then(() => {
console.log("Promise inside Timeout");
});
}, 0);
setTimeout(() => {
console.log("Timeout 2");
}, 0);
Promise.resolve().then(() => {
console.log("Promise 1");
setTimeout(() => {
console.log("Timeout inside Promise");
}, 0);
});
Promise.resolve().then(() => {
console.log("Promise 2");
});
console.log("End");
Output:
Start
End
Promise 1
Promise 2
Timeout 1
Promise inside Timeout
Timeout 2
Timeout inside Promise
Step-by-step:
"Start"-> prints (sync)setTimeout(Timeout 1)-> Callback QueuesetTimeout(Timeout 2)-> Callback QueuePromise.then(Promise 1)-> Microtask QueuePromise.then(Promise 2)-> Microtask Queue"End"-> prints (sync)- Drain Microtask Queue:
"Promise 1"prints -> registerssetTimeout(Timeout inside Promise)-> goes to Callback Queue"Promise 2"prints
- Pick one macrotask:
"Timeout 1"prints -> registersPromise.then(Promise inside Timeout)-> goes to Microtask Queue - Drain Microtask Queue:
"Promise inside Timeout"prints - Pick one macrotask:
"Timeout 2"prints - Check microtasks (empty) -> Pick one macrotask:
"Timeout inside Promise"prints
Predict the Output — Level 4 (async/await Edition)
async function foo() {
console.log("foo start");
await bar();
console.log("foo end");
}
async function bar() {
console.log("bar");
}
console.log("script start");
foo();
console.log("script end");
Output:
script start
foo start
bar
script end
foo end
Why? await bar() pauses foo after bar() executes synchronously. The code after await ("foo end") is essentially a .then() callback — it goes to the Microtask Queue. "script end" runs first (synchronous), then "foo end" runs from the microtask queue.
queueMicrotask() — Explicitly Add to Microtask Queue
console.log("1");
queueMicrotask(() => {
console.log("2 — queueMicrotask");
});
Promise.resolve().then(() => {
console.log("3 — Promise.then");
});
setTimeout(() => {
console.log("4 — setTimeout");
}, 0);
console.log("5");
Output:
1
5
2 — queueMicrotask
3 — Promise.then
4 — setTimeout
queueMicrotask and Promise.then both go to the Microtask Queue and execute in the order they were queued — both before setTimeout.
Common Mistakes
- Predicting output as "A, D, B, C" when it's actually "A, D, C, B" — forgetting that Promises always beat
setTimeout(fn, 0). - Thinking code after
awaitruns synchronously after the awaited value resolves. It is actually re-entered as a microtask on the next tick. - Assuming
queueMicrotask(fn)andsetTimeout(fn, 0)are equivalent. They live in different queues and have opposite priority.
Interview Questions
Q: What is the Microtask Queue?
The Microtask Queue is a high-priority queue where callbacks from
Promise.then/.catch/.finally,async/awaitcontinuations,MutationObserver, andqueueMicrotask()are placed. The Event Loop drains the entire Microtask Queue before picking the next macrotask from the Callback Queue.
Q: Why do Promises execute before setTimeout even with 0ms delay?
Because Promise callbacks go to the Microtask Queue, while
setTimeoutcallbacks go to the Callback Queue (Macrotask Queue). The Event Loop always checks and drains the Microtask Queue completely before picking the next macrotask. Priority: Microtasks > Macrotasks.
Q: What happens to code after await in an async function?
Code after
awaitis wrapped in an implicit.then()callback and placed in the Microtask Queue. The async function pauses at theawaitpoint, control returns to the caller, and the remaining code runs as a microtask when the awaited value resolves.
Q: How many microtasks are drained per Event Loop tick?
All of them. The Event Loop keeps draining the Microtask Queue until it's empty — even if new microtasks are added during draining — before it moves on to pick the next macrotask.
Q: What is queueMicrotask() and when would you use it?
queueMicrotask(fn)explicitly schedulesfnin the Microtask Queue. It's useful when you need something to run after the current synchronous block but before any I/O, timers, or rendering — for example, to batch DOM reads/writes or to defer state notifications without the overhead of creating a Promise.
Quick Reference — Cheat Sheet
MICROTASK QUEUE — QUICK MAP
Priority: HIGHER than the Callback Queue (macrotasks).
Drain rule: Event Loop empties ENTIRE microtask queue
before picking ONE macrotask.
Sources:
Promise.then / .catch / .finally
async/await (code after await)
MutationObserver
queueMicrotask(fn)
Tick order:
1. Run sync code to completion
2. Drain microtasks (all of them, including newly added ones)
3. Pick ONE macrotask
4. Repeat from step 2
Gotcha:
setTimeout(fn, 0) < Promise.resolve().then(fn)
async fn code after `await` is a microtask
Previous: Callback Queue -> The Macrotask Line Next: Starvation of Callbacks -> When Microtasks Hog the Loop
This is Lesson 3.4 of the JavaScript Interview Prep Course — 14 chapters, 87 lessons.