What is the microtask queue?

JavaScript

The short answer

The microtask queue is a special queue in JavaScript's event loop that has higher priority than the regular callback queue (also called the task queue or macrotask queue). Promises (.then, .catch, .finally), queueMicrotask, and MutationObserver callbacks go into the microtask queue. After every task completes, the event loop drains the entire microtask queue before picking anything from the callback queue.

Why it exists

JavaScript has one thread and two queues for asynchronous work:

  1. Microtask queue — for promise callbacks, queueMicrotask, MutationObserver
  2. Macrotask queue (callback queue) — for setTimeout, setInterval, I/O, UI events

The microtask queue exists because some async operations need to complete before the browser does anything else. Promises, for example, should resolve as soon as possible. You do not want a setTimeout from somewhere else to run in between a promise chain.

How it works step by step

Let me walk you through this example:

console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');

Output: 1, 4, 3, 2

Here is what happens:

Step 1: console.log('1') runs immediately. Prints 1.

Step 2: setTimeout schedules a callback in the macrotask queue. The timer starts (0ms, but still async).

Step 3: Promise.resolve().then(...) schedules a callback in the microtask queue.

Step 4: console.log('4') runs immediately. Prints 4.

Step 5: The call stack is empty. The event loop checks the microtask queue first. It finds the promise callback, runs it. Prints 3.

Step 6: Microtask queue is empty. The event loop checks the macrotask queue. It finds the setTimeout callback, runs it. Prints 2.

The key rule: microtasks always run before macrotasks.

Microtasks can schedule more microtasks

Here is where it gets interesting. If a microtask adds another microtask, the new one also runs before any macrotask. The event loop keeps draining the microtask queue until it is completely empty.

console.log('Start');
setTimeout(() => console.log('Timeout'), 0);
Promise.resolve()
.then(() => {
console.log('Promise 1');
return Promise.resolve();
})
.then(() => {
console.log('Promise 2');
});
Promise.resolve().then(() => {
console.log('Promise 3');
});
console.log('End');

Output:

Start
End
Promise 1
Promise 3
Promise 2
Timeout

Let me explain:

  1. Start and End run synchronously
  2. The microtask queue has two items: the first .then and Promise 3
  3. Promise 1 runs. It returns a new promise, so Promise 2 is added to the microtask queue
  4. Promise 3 runs (it was already in the queue)
  5. Promise 2 runs (it was just added)
  6. Microtask queue is now empty, so Timeout runs from the macrotask queue

queueMicrotask

You can manually add something to the microtask queue:

console.log('Start');
queueMicrotask(() => {
console.log('Microtask');
});
console.log('End');
// Output: Start, End, Microtask

This runs after the current synchronous code finishes but before any macrotasks. It is useful when you need something to run "as soon as possible" without waiting for the next macrotask.

Microtasks and rendering

Here is something important for frontend developers. The browser renders the page between macrotasks, but not between microtasks. This means if your microtasks take too long or keep adding more microtasks, the page will freeze because the browser cannot repaint.

// This will freeze the browser!
function infiniteMicrotask() {
Promise.resolve().then(infiniteMicrotask);
}
infiniteMicrotask();

Each microtask adds another microtask. The event loop keeps draining the microtask queue and never gets to the macrotask queue or the rendering step. The page becomes unresponsive.

Common Pitfalls

An infinite microtask loop is worse than an infinite setTimeout loop. With setTimeout, the browser gets a chance to render between each iteration. With microtasks, it does not. If you ever see a page freeze with no apparent infinite loop, check if microtasks are recursively scheduling more microtasks.

The complete event loop order

Here is the full priority order in each iteration of the event loop:

  1. Run all synchronous code in the call stack
  2. Drain the entire microtask queue (run all microtasks, including new ones added during this step)
  3. Render if needed (paint, layout)
  4. Pick one macrotask from the macrotask queue and run it
  5. Go back to step 2

This is why promises resolve before setTimeout — they are microtasks, and step 2 always runs before step 4.

Practical use case — batching updates

Microtasks are useful for batching work. React uses this concept internally for state updates:

// Simplified concept
let pendingUpdates = [];
function setState(update) {
pendingUpdates.push(update);
// Schedule processing after current sync code
queueMicrotask(() => {
const updates = [...pendingUpdates];
pendingUpdates = [];
applyUpdates(updates);
});
}

Multiple setState calls in the same synchronous block all go into pendingUpdates. The microtask runs after the sync code, processes all updates at once, and re-renders only once.

Interview Tip

When explaining the microtask queue, always use a code example that mixes console.log, setTimeout, and Promise.resolve().then(). Walk through it step by step and predict the output. The interviewer is testing if you can trace the execution order. The most important thing to remember is: microtasks always run before macrotasks and before the browser renders.

Why interviewers ask this

The microtask queue is a deep concept that shows you understand how JavaScript's event loop really works. It is not enough to know "promises run before setTimeout" — interviewers want to know why. They want to see if you can trace through complex async code, predict the output correctly, and understand the implications for performance and rendering. This question separates candidates with surface-level knowledge from those who truly understand JavaScript's execution model.