Microtasks vs Macrotasks: Predicting Output Order
Once you know how the event loop works, you can predict the exact output order of any async code. The key is knowing which callbacks are microtasks and which are macrotasks.
Definition
- A macrotask (also called a task) is a callback that waits in the callback queue, such as a
setTimeoutcallback or a click handler. - A microtask is a callback that waits in the microtask queue, such as a promise
.thencallback. Microtasks have higher priority.
Analogy: think of a doctor's clinic. Macrotasks are patients with appointments, seen one at a time. Microtasks are emergencies. After every patient, the doctor first handles all emergencies waiting, and only then calls the next patient.
| Type | Examples | Queue |
|---|---|---|
| Macrotask | setTimeout, setInterval, DOM events (click), network events, script loading | Callback queue (task queue) |
| Microtask | .then, .catch, .finally, code after await, queueMicrotask, MutationObserver | Microtask queue |
The Rules for Predicting Output
- Run all synchronous code first, top to bottom.
- Then run all microtasks, until the microtask queue is empty. Microtasks added during this step also run now.
- Then run one macrotask.
- After that macrotask, run all microtasks again.
- Repeat steps 3 and 4.
Two things that surprise people:
- The function passed to
new Promise(...)runs synchronously. Only.thenis a microtask. - In an
asyncfunction, code before the firstawaitruns synchronously. Code afterawaitis a microtask.
Example 1: The Basic Case
console.log("1");setTimeout(() => console.log("2"), 0);Promise.resolve().then(() => console.log("3"));console.log("4");// Output: 1, 4, 3, 2
| Step | Runs | Why |
|---|---|---|
| 1 | 1, 4 | Synchronous code |
| 2 | 3 | Microtask (promise) |
| 3 | 2 | Macrotask (timer), even with a 0 ms delay |
Example 2: The Promise Constructor Is Synchronous
console.log("A");new Promise((resolve) => {console.log("B"); // runs immediatelyresolve();}).then(() => console.log("C"));console.log("D");// Output: A, B, D, C
B prints right away, because the executor function runs synchronously. Only the .then callback (C) waits in the microtask queue.
Example 3: Nested Tasks
console.log("A");setTimeout(() => {console.log("B");Promise.resolve().then(() => console.log("C"));}, 0);Promise.resolve().then(() => {console.log("D");setTimeout(() => console.log("E"), 0);});console.log("F");// Output: A, F, D, B, C, E
| Step | Runs | What happens |
|---|---|---|
| 1 | A, F | Synchronous code. Timer B goes to the callback queue, promise D to the microtask queue |
| 2 | D | Microtask. It schedules timer E behind timer B |
| 3 | B | First macrotask. It adds microtask C |
| 4 | C | Microtasks run after every macrotask, so C goes before E |
| 5 | E | Next macrotask |
Example 4: async / await
console.log("1");setTimeout(() => console.log("6"), 0);async function run() {console.log("2"); // synchronousawait null;console.log("4"); // microtask}run();Promise.resolve().then(() => console.log("5"));console.log("3");// Output: 1, 2, 3, 4, 5, 6
2prints synchronously, because it is before theawait.4is queued as a microtask first (whenrun()hitsawait), so it runs before5.6is a macrotask, so it runs last.
Common Mistakes
| Mistake | Correct idea |
|---|---|
"setTimeout(fn, 0) runs immediately" | It runs only after sync code and all microtasks finish |
| "Everything in a promise is async" | The executor of new Promise is synchronous |
"All of an async function runs later" | Code before the first await runs synchronously |
| "Microtasks run once at the start" | Microtasks run after every macrotask |
Summary
- Macrotasks:
setTimeout,setInterval, DOM events. Microtasks: promise callbacks, code afterawait,queueMicrotask,MutationObserver. - Order: sync code → all microtasks → one macrotask → all microtasks → ...
- Microtasks always beat macrotasks, even a
setTimeoutwith 0 ms. - The
new Promiseexecutor and code beforeawaitare synchronous. - Microtasks created inside a macrotask run before the next macrotask.
Interview Answer: What Is the Difference Between Microtasks and Macrotasks?
Macrotasks are callbacks from setTimeout, setInterval, and DOM events, and they wait in the callback queue. Microtasks are callbacks from promises (.then, .catch, .finally), code after await, queueMicrotask, and MutationObserver, and they wait in the microtask queue.
The event loop first runs all synchronous code. Then it empties the entire microtask queue, and only then runs one macrotask. After every macrotask, it empties the microtask queue again. Because of this, a promise callback always runs before a setTimeout callback, even when the timer is set to 0 ms.
To predict output, I list the sync logs first, then the microtasks in the order they were queued, and then the macrotasks, remembering that the new Promise executor and code before await are synchronous.