Q26 of 40 · JavaScript
What is the difference between microtasks and macrotasks in the JavaScript event loop?
Short answer
Short answer: Microtasks (Promise callbacks, queueMicrotask) run immediately after the current task and drain completely before the next macrotask. Macrotasks (setTimeout, setInterval, I/O) are scheduled once per event loop iteration. This ordering means Promises always resolve before the next timer fires.
Detail
The event loop processes tasks from two queues with different priorities.
Macrotask queue (task queue): One task is dequeued per event loop iteration. Sources include: setTimeout, setInterval, setImmediate (Node), I/O callbacks, UI rendering.
Microtask queue: After each macrotask completes (and after the current synchronous script if it's the first iteration), the event loop drains the entire microtask queue before picking the next macrotask. Sources include: Promise.then/catch/finally, queueMicrotask, MutationObserver callbacks, async function continuations after await.
Practical consequence: If a Promise.then callback schedules another Promise.then, it runs before any setTimeout — the microtask queue re-drains after every microtask. This can starve the macrotask queue if microtasks enqueue indefinitely.
In test debugging: Knowing this priority explains why await Promise.resolve() flushes all pending Promise callbacks — a useful technique in tests to let all microtasks settle before asserting.
// EXAMPLE
console.log("1 sync");
setTimeout(() => console.log("5 setTimeout"), 0); // macrotask
Promise.resolve()
.then(() => {
console.log("3 microtask 1");
return Promise.resolve();
})
.then(() => console.log("4 microtask 2")); // queued after microtask 1
console.log("2 sync end");
// Output: 1, 2, 3, 4, 5
// All microtasks drain before setTimeout fires
// Test helper: flush microtasks
async function flushMicrotasks() {
await Promise.resolve(); // yields to drain microtask queue
}