JavaScript Interview Prep
Asynchronous JavaScript

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, and queueMicrotask all feed a different queue — the Microtask Queue — and the Event Loop drains EVERY microtask before it touches a single setTimeout. Even setTimeout(fn, 0) cannot cut that line.

This one detail is the reason async/await continuations feel "instant," why Promise.then chaining 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 for queueMicrotask().

Read the full lesson -> [link]

#JavaScript #Promises #Microtasks #AsyncAwait #EventLoop #InterviewPrep #AsyncJS


Microtask Queue thumbnail


What You'll Learn

  • What goes into the Microtask Queue and why it always beats the Callback Queue
  • How async/await continuations 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?

SourceExample
Promise.then / .catch / .finallyPromise.resolve().then(cb)
async/awaitCode after await
MutationObserverDOM 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. "1" -> prints (synchronous)
  2. setTimeout callback -> Callback Queue
  3. First Promise.resolve().then(() => log("3")) -> Microtask Queue
  4. The .then(() => log("4")) is NOT queued yet — it chains after "3" resolves
  5. Second Promise.resolve().then(() => log("5")) -> Microtask Queue
  6. "6" -> prints (synchronous)
  7. 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)
  8. 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:

  1. "Start" -> prints (sync)
  2. setTimeout(Timeout 1) -> Callback Queue
  3. setTimeout(Timeout 2) -> Callback Queue
  4. Promise.then(Promise 1) -> Microtask Queue
  5. Promise.then(Promise 2) -> Microtask Queue
  6. "End" -> prints (sync)
  7. Drain Microtask Queue:
    • "Promise 1" prints -> registers setTimeout(Timeout inside Promise) -> goes to Callback Queue
    • "Promise 2" prints
  8. Pick one macrotask: "Timeout 1" prints -> registers Promise.then(Promise inside Timeout) -> goes to Microtask Queue
  9. Drain Microtask Queue: "Promise inside Timeout" prints
  10. Pick one macrotask: "Timeout 2" prints
  11. 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.

Microtask Queue visual 1


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 await runs synchronously after the awaited value resolves. It is actually re-entered as a microtask on the next tick.
  • Assuming queueMicrotask(fn) and setTimeout(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/await continuations, MutationObserver, and queueMicrotask() 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 setTimeout callbacks 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 await is wrapped in an implicit .then() callback and placed in the Microtask Queue. The async function pauses at the await point, 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 schedules fn in 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.

On this page