Difference between microtasks and macrotasks in JavaScript

Advanced14 min interview
Skills tested:
Identifying which APIs produce microtasks vs macrotasksUnderstanding the drain-all-microtasks rule between macrotasksPredicting output order for mixed synchronous, Promise, and setTimeout codeMicrotask starvation: when chained microtasks block macrotask processingHow MutationObserver fits into the microtask queue

Advertisement

🧩 Scenario

In a real codebase, the microtask vs macrotask distinction matters when you need to schedule work that should run before the browser renders the next frame, when you are debugging unexpected output ordering in async code, or when you are diagnosing a UI that freezes because a recursive Promise chain starves the rendering pipeline.

Architecture Walkthrough

The Two Queue Types

The JavaScript event loop manages two distinct callback queues. The macrotask queue (also called the task queue) holds callbacks from setTimeout, setInterval, setImmediate (Node.js), I/O completions, and MessageChannel. The microtask queue holds callbacks from Promise .then, .catch, .finally, queueMicrotask(), and MutationObserver. The initial script execution is itself treated as a macrotask.

The fundamental rule that governs their interaction is: after each macrotask completes, the event loop drains the entire microtask queue before picking up the next macrotask. Every pending microtask runs, and if any microtask callback enqueues another microtask, that new microtask also runs in the same drain cycle. Only when the microtask queue is completely empty does the event loop proceed to the next macrotask.

Drain-All Rule and Output Ordering

This drain-all rule is the reason Promise callbacks always run before setTimeout callbacks even when the timeout delay is zero. Both the Promise .then callback and the setTimeout callback are ready at the same moment. The Promise callback is in the microtask queue; the setTimeout callback is in the macrotask queue. After the current synchronous code finishes (the end of the current macrotask), the microtask queue drains completely before the next macrotask runs.

A chained .then inside a .then is still a microtask. It is enqueued during the drain cycle, and the drain continues to include it. No setTimeout callback can run until the entire chain of dependent .then callbacks has resolved.

Microtask Starvation

Because the microtask queue is drained completely between each macrotask, a callback that continuously enqueues new microtasks can starve the macrotask queue indefinitely. If a microtask registers another microtask in a recursive pattern, the setTimeout callbacks waiting in the macrotask queue will never run. This is not a hypothetical concern: it can prevent rendering (which is triggered between macrotasks in the browser), block I/O callbacks, and freeze the UI.

requestAnimationFrame runs after the microtask drain but before the browser paints. It is neither a microtask nor a classic macrotask but sits in a separate rendering pipeline step.


Key Code Explained

console.log('1 — sync');

setTimeout(() => console.log('5 — macrotask'), 0);

Promise.resolve()
  .then(() => {
    console.log('2 — microtask');
    return Promise.resolve(); // creates a new microtask for the next .then
  })
  .then(() => console.log('3 — microtask (chained)'));

queueMicrotask(() => console.log('4 — microtask (queueMicrotask)'));

console.log('6 — sync');

// Output: 1, 6, 2, 4, 3, 5

Walking through the execution: 1 and 6 run synchronously during the current macrotask. When the call stack empties, the microtask queue drains. The first .then callback prints 2 and resolves, enqueueing the chained .then. The queueMicrotask callback prints 4. The newly enqueued chained .then prints 3. All microtasks are now done. The event loop picks up the macrotask queue and prints 5.

// Microtask starvation: macrotask callbacks are blocked
function recursiveMicrotask() {
  Promise.resolve().then(recursiveMicrotask); // queues a new microtask on every run
}
recursiveMicrotask();
setTimeout(() => console.log('This will never print'), 100);
// The setTimeout callback starves because microtasks drain endlessly

Tradeoffs

Queue typeSourcesWhen it runs
Macrotask queuesetTimeout, setInterval, I/O, MessageChannelOne per event loop tick, after microtask queue is empty
Microtask queuePromise.then, queueMicrotask, MutationObserverFully drained after each macrotask, before next macrotask
Rendering pipelinerequestAnimationFrame, layout, paintAfter microtask drain, before browser paints

What Interviewers Actually Check

  • Whether you can name concrete sources of microtasks and macrotasks
  • Whether you can correctly state and apply the drain-all-microtasks rule
  • Whether you can predict output order for mixed code accurately
  • Whether you know that a chained .then inside a .then is still a microtask and still runs before any setTimeout
  • Whether you know what microtask starvation is and how it can freeze the UI

Follow-Up Questions

  1. What is queueMicrotask() and when would you use it instead of Promise.resolve().then()?
  2. How does Node.js process.nextTick relate to the microtask queue and does it run before or after Promise callbacks?
  3. If await somePromise is inside an async function, where does the code after await run in the queue hierarchy?
  4. How does MutationObserver use the microtask queue and why is that timing important for DOM batch updates?
  5. A teammate proposes using a recursive Promise.resolve().then() loop for a polling mechanism. What concern would you raise?

Common Candidate Mistakes

  • Saying Promise callbacks and setTimeout(fn, 0) run at the same priority when microtasks always win the ordering
  • Not knowing that chained .then callbacks inside .then callbacks are all microtasks and all drain before any macrotask
  • Misplacing requestAnimationFrame in either the microtask or macrotask queue when it is part of a separate rendering step
  • Not knowing that queueMicrotask() exists and that you can explicitly enqueue a microtask without wrapping in a Promise
  • Not being able to trace output order in a mixed code example, which is one of the most common senior JavaScript interview questions

Interview Readiness Checklist

Before you leave this question, make sure you can answer:

  • Can you name three sources of microtasks and three sources of macrotasks?
  • Can you explain the drain-all-microtasks rule in plain terms?
  • Can you predict the output order of code that mixes synchronous statements, setTimeout, and Promise callbacks?
  • Can you explain what happens when a microtask callback enqueues another microtask?
  • Can you describe microtask starvation and when it becomes a real problem?

Summary

The JavaScript event loop processes one macrotask at a time. Between macrotasks, it drains the entire microtask queue: every pending microtask runs, and if any microtask enqueues another microtask, that new one also runs before the next macrotask is picked up. This drain-all rule is specified in the HTML specification and is not an implementation detail.

Microtasks come from Promise callbacks (.then, .catch, .finally), queueMicrotask(), and MutationObserver. Macrotasks come from setTimeout, setInterval, I/O callbacks, and MessageChannel. The initial script execution is itself a macrotask, which is why all synchronous code runs before any queued callbacks.

The practical implication is that Promise.resolve().then(fn) always runs before setTimeout(fn, 0) even if both are scheduled at the same instant. Understanding this ordering is essential for predicting async output in interviews and for diagnosing subtle bugs in production where async callbacks run in an unexpected sequence.

Related Videos
Watch these videos to learn more about this topic
Frequently Asked Questions

Are Promises microtasks or macrotasks?

Promise callbacks (.then, .catch, .finally) are microtasks. setTimeout and setInterval are macrotasks.

Advertisement


Stay Updated

Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.

Advertisement