Difference between microtasks and macrotasks in JavaScript
Advertisement
🧩 Scenario
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 type | Sources | When it runs |
|---|---|---|
| Macrotask queue | setTimeout, setInterval, I/O, MessageChannel | One per event loop tick, after microtask queue is empty |
| Microtask queue | Promise.then, queueMicrotask, MutationObserver | Fully drained after each macrotask, before next macrotask |
| Rendering pipeline | requestAnimationFrame, layout, paint | After 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
.theninside a.thenis still a microtask and still runs before anysetTimeout - Whether you know what microtask starvation is and how it can freeze the UI
Follow-Up Questions
- What is
queueMicrotask()and when would you use it instead ofPromise.resolve().then()? - How does Node.js
process.nextTickrelate to the microtask queue and does it run before or after Promise callbacks? - If
await somePromiseis inside an async function, where does the code afterawaitrun in the queue hierarchy? - How does
MutationObserveruse the microtask queue and why is that timing important for DOM batch updates? - 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
.thencallbacks inside.thencallbacks are all microtasks and all drain before any macrotask - Misplacing
requestAnimationFramein 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, andPromisecallbacks? - 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.
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