On this page

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 setTimeout callback or a click handler.
  • A microtask is a callback that waits in the microtask queue, such as a promise .then callback. 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.

TypeExamplesQueue
MacrotasksetTimeout, setInterval, DOM events (click), network events, script loadingCallback queue (task queue)
Microtask.then, .catch, .finally, code after await, queueMicrotask, MutationObserverMicrotask queue

The Rules for Predicting Output

  1. Run all synchronous code first, top to bottom.
  2. Then run all microtasks, until the microtask queue is empty. Microtasks added during this step also run now.
  3. Then run one macrotask.
  4. After that macrotask, run all microtasks again.
  5. Repeat steps 3 and 4.

Two things that surprise people:

  • The function passed to new Promise(...) runs synchronously. Only .then is a microtask.
  • In an async function, code before the first await runs synchronously. Code after await is 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
StepRunsWhy
11, 4Synchronous code
23Microtask (promise)
32Macrotask (timer), even with a 0 ms delay

Example 2: The Promise Constructor Is Synchronous

console.log("A");
new Promise((resolve) => {
console.log("B"); // runs immediately
resolve();
}).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
StepRunsWhat happens
1A, FSynchronous code. Timer B goes to the callback queue, promise D to the microtask queue
2DMicrotask. It schedules timer E behind timer B
3BFirst macrotask. It adds microtask C
4CMicrotasks run after every macrotask, so C goes before E
5ENext macrotask

Example 4: async / await

console.log("1");
setTimeout(() => console.log("6"), 0);
async function run() {
console.log("2"); // synchronous
await null;
console.log("4"); // microtask
}
run();
Promise.resolve().then(() => console.log("5"));
console.log("3");
// Output: 1, 2, 3, 4, 5, 6
  • 2 prints synchronously, because it is before the await.
  • 4 is queued as a microtask first (when run() hits await), so it runs before 5.
  • 6 is a macrotask, so it runs last.

Common Mistakes

MistakeCorrect 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 after await, queueMicrotask, MutationObserver.
  • Order: sync code → all microtasks → one macrotask → all microtasks → ...
  • Microtasks always beat macrotasks, even a setTimeout with 0 ms.
  • The new Promise executor and code before await are 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.

Next TopicSingle-Threaded Model and libuv