Microtasks & Macrotasks
Two queues, different priorities. Why Promises always beat setTimeout.
Two Queues
Not all async callbacks are equal.
Microtask Queue
- • Promise .then/.catch/.finally
- • async/await continuations
- • queueMicrotask()
- • MutationObserver
HIGH PRIORITY — drain ALL
Macrotask Queue
- • setTimeout / setInterval
- • DOM events (click, etc.)
- • fetch response callbacks
- • requestAnimationFrame*
LOWER PRIORITY — one at a time
// Quick proof — microtasks beat macrotasks:
setTimeout(() => console.log("macro"), 0);
Promise.resolve().then(() => console.log("micro"));
console.log("sync");
// Output: sync, micro, macro
// Even though both have 0 delay,
// microtask runs first because it has priorityMicrotasks
High-priority tasks that drain completely before anything else.
// Promise callbacks are microtasks:
Promise.resolve()
.then(() => console.log("micro 1"))
.then(() => console.log("micro 2"))
.then(() => console.log("micro 3"));
// async/await — code after await is a microtask:
async function example() {
console.log("before await"); // sync
await fetch("/api"); // pauses here
console.log("after await"); // microtask!
}
// queueMicrotask — explicitly schedule a microtask:
queueMicrotask(() => {
console.log("explicit microtask");
});
// MutationObserver — DOM mutation callbacks:
const observer = new MutationObserver(() => {
console.log("DOM changed"); // microtask
});// Key rule: ALL microtasks drain before moving on
// Even microtasks created during microtask processing!
Promise.resolve().then(() => {
console.log("micro 1");
// This creates a NEW microtask — it runs BEFORE any macrotask:
Promise.resolve().then(() => console.log("micro 2"));
});
setTimeout(() => console.log("macro"), 0);
// Output: micro 1, micro 2, macro
// micro 2 was created inside micro 1, but still runs
// before the macrotask — the queue is fully drainedMacrotasks
Regular tasks — one per event loop iteration.
// setTimeout/setInterval — classic macrotasks:
setTimeout(() => console.log("task 1"), 0);
setTimeout(() => console.log("task 2"), 0);
setTimeout(() => console.log("task 3"), 0);
// Each runs in a separate event loop iteration:
// Iteration 1: task 1 → drain microtasks → render
// Iteration 2: task 2 → drain microtasks → render
// Iteration 3: task 3 → drain microtasks → render
// DOM events are also macrotasks:
button.addEventListener("click", () => {
console.log("click handler"); // macrotask
Promise.resolve().then(() => console.log("micro in click"));
});
// When clicked: "click handler", "micro in click"
// The microtask runs before the next macrotask/render
// I/O callbacks (fetch completion, file read):
fetch("/api").then(r => r.json()).then(data => {
// The .then is a microtask, but the fetch completion
// that triggers it originates as a macrotask
});Priority Rules
The complete execution order.
// The event loop cycle:
// 1. Execute synchronous code (call stack)
// 2. Drain ALL microtasks
// 3. ONE macrotask
// 4. Drain ALL microtasks (again)
// 5. Render (if needed)
// 6. Repeat from step 3
console.log("1: sync");
setTimeout(() => {
console.log("4: macro 1");
Promise.resolve().then(() => console.log("5: micro inside macro"));
}, 0);
setTimeout(() => {
console.log("6: macro 2");
}, 0);
Promise.resolve().then(() => console.log("2: micro 1"));
Promise.resolve().then(() => console.log("3: micro 2"));
// Output: 1, 2, 3, 4, 5, 6Sync runs first, then ALL microtasks, then ONE macrotask, then ALL microtasks created during that macrotask, then next macrotask.
1 / 2
Starvation
Microtasks can block everything else.
// ⚠️ DANGER: Microtasks that create more microtasks
// can starve macrotasks and rendering:
function infiniteMicrotasks() {
Promise.resolve().then(() => {
console.log("micro");
infiniteMicrotasks(); // creates another microtask
});
}
infiniteMicrotasks();
// The microtask queue NEVER empties!
// setTimeout callbacks never run
// The page never renders
// Same effect as an infinite while loop
// Safe pattern — break work into macrotasks:
function processItems(items) {
if (items.length === 0) return;
const item = items.shift();
process(item);
setTimeout(() => processItems(items), 0); // yields to render
}
// Or use requestIdleCallback for low-priority work:
requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0 && tasks.length) {
tasks.shift()();
}
});Rule of thumb:
Microtasks for quick continuations (promise chains). Macrotasks (setTimeout) when you need to yield to the browser for rendering or user input.
FAQ
Common questions about task scheduling.