JS Microtasks vs Macrotasks: A Complete Guide
Microtasks and macrotasks are the two queue types that determine async callback order in JavaScript. Learn their differences and why Promise callbacks run before setTimeout.
Microtasks vs macrotasks describes the two asynchronous queues in JavaScript: the microtask queue and the macrotask queue (also called the task queue). Every async callback enters one of these two queues. Which queue it enters determines when it runs relative to other callbacks.
Microtasks have higher priority. The event loop drains the entire microtask queue before taking a single macrotask. This is why a Promise.then callback always runs before a setTimeout callback registered at the same time.
Quick Comparison
| Microtask | Macrotask (Task) | |
|---|---|---|
| Sources | Promise.then/catch/finally, queueMicrotask, MutationObserver | setTimeout, setInterval, event listeners, fetch, I/O |
| Processing | Entire queue drained at once | One callback per iteration |
| Can add more | Yes, and they run in the same cycle | Yes, but they wait for the next iteration |
| Blocks rendering | Yes, if microtasks keep adding more | One long task can block |
| Use case | Work that must finish before the next render | Deferred work, yielding to the event loop |
Where Each Queue Fits in the Event Loop
The event loop follows a fixed sequence. Understanding where microtasks and macrotasks fit in that sequence is essential.
Each iteration takes one macrotask, runs it to completion, then drains every microtask including any new ones added during draining. Only after the microtask queue is empty does the next macrotask begin. Between macrotasks, the browser may also render.
Visual Example: Execution Order
Here is code that mixes microtasks and macrotasks to show the order clearly.
console.log("1: sync");
setTimeout(() => console.log("2: macrotask"), 0);
Promise.resolve().then(() => console.log("3: microtask"));
console.log("4: sync");The output is predictable: 1, 4, 3, 2. Both synchronous calls run first because they are on the call stack. The Promise handler enters the microtask queue and runs before the setTimeout callback in the macrotask queue.
The delay argument to setTimeout does not change priority. Even with a delay of zero milliseconds, the macrotask queue is always processed after the microtask queue.
How Microtasks Can Starve Macrotasks
Because the microtask queue drains completely, a microtask that recursively adds more microtasks can block macrotasks and rendering indefinitely.
function recursiveMicrotask(count) {
if (count <= 0) return;
console.log("Microtask", count);
Promise.resolve().then(() => recursiveMicrotask(count - 1));
}
recursiveMicrotask(1000);
setTimeout(() => console.log("This never prints first"), 0);All one thousand microtask callbacks run before the setTimeout callback. The browser cannot render or process user input until the microtask chain finishes. This is why queueMicrotask and recursive Promise chains should be used carefully.
For more on event loop architecture, see /javascript/the-js-event-loop-architecture-complete-guide.
Which APIs Feed Which Queue
Knowing the source of each callback type helps you predict execution order.
| API | Queue Type | When Callback Enters Queue |
|---|---|---|
| Promise.then/catch/finally | Microtask | Immediately when the promise settles |
| queueMicrotask(fn) | Microtask | Immediately |
| MutationObserver | Microtask | After observed DOM mutations |
| setTimeout(fn, ms) | Macrotask | After ms milliseconds elapse |
| setInterval(fn, ms) | Macrotask | Every ms milliseconds |
| addEventListener("click", fn) | Macrotask | When the user clicks |
| fetch().then(fn) | Macrotask (fetch), Microtask (.then) | fetch completes, then .then is queued |
| requestAnimationFrame(fn) | Render queue | Before the next paint |
| process.nextTick(fn) | Node.js nextTick | Highest priority in Node.js |
The fetch API is interesting because it spans both queues. The network request itself is handled as a macrotask. When the response arrives, the Promise resolves and its .then handler enters the microtask queue.
Macrotasks for Yielding to the Event Loop
One practical use of macrotasks is yielding control back to the event loop. A long synchronous operation can be broken into chunks using setTimeout with a delay of zero.
function processLargeArray(items, chunkSize) {
let index = 0;
function nextChunk() {
const end = Math.min(index + chunkSize, items.length);
for (let i = index; i < end; i += 1) {
items[i] = items[i] * 2;
}
index = end;
if (index < items.length) {
setTimeout(nextChunk, 0);
}
}
nextChunk();
}Each chunk runs as a separate macrotask. Between chunks, the event loop can process microtasks, handle user input, and render updates. Without this pattern, the entire operation would block the page until complete.
Microtasks for Batching Updates
Microtasks are useful when you need work to complete before the browser renders. Framework state updates often use microtasks for this reason.
let state = { count: 0 };
function updateState(changes) {
Object.assign(state, changes);
queueMicrotask(() => {
console.log("State updated:", state.count);
});
}
updateState({ count: 1 });
updateState({ count: 2 });
updateState({ count: 3 });All three updateState calls run synchronously, mutating the state object. A single microtask then logs the final state. Only one microtask runs, not three, because each queueMicrotask call is separate but they all run before any macrotask.
Common Mistake: Confusing Timing with Priority
A setTimeout callback with a delay of 100ms does not have lower priority than one with 0ms. Both are macrotasks with equal priority. The delay only determines when each callback enters the queue, not its priority within the queue.
Once a macrotask enters the queue, it waits for the current task to finish and for all microtasks to drain. Two setTimeout callbacks with delays of 0ms and 100ms both wait equally for the microtask queue to empty. The 0ms callback enters the queue sooner, but both have the same priority once they are in the queue.
For timing details on setTimeout specifically, see /javascript/javascript-settimeout-behavior-complete-guide.
Rune AI
Key Insights
- Microtasks come from Promises and queueMicrotask. They drain completely after every macrotask.
- Macrotasks come from setTimeout, event listeners, and I/O. One runs per event loop iteration.
- Microtasks always run before the next macrotask, regardless of timing.
- Microtasks can add more microtasks, which all run in the same cycle.
- Choose microtasks for work that must complete before the next render frame.
Frequently Asked Questions
Which is faster: microtask or macrotask?
Can I convert a macrotask into a microtask?
Conclusion
Microtasks and macrotasks are the two lanes of the event loop. Microtasks have higher priority and drain completely between macrotasks. Macrotasks run one at a time per event loop iteration. Knowing which APIs feed into which queue is the key to predicting async execution order.
More in this topic
Using Decorators for Logging in JS Architecture
Learn how JavaScript decorators wrap class methods to add logging without touching the original code, including setup with Babel and how execution order works.
JavaScript While Loop Explained: A Complete Guide
A while loop repeats code as long as a condition stays true. Learn the syntax, see practical examples, and understand when a while loop is the right choice over a for loop.
JavaScript Loops Tutorial: for, while, do while
Loops let JavaScript repeat code without writing it twice. Learn the three core loop types, what each one does, and when to choose one over another.