Beyond the basic Call Stack / Web APIs / Callback Queue model, professional JavaScript work requires precisely predicting execution order — and that means understanding the distinct microtask and macrotask queues the event loop actually processes.
1The Event Loop: Microtask/Macrotask Ordering | JavaScript Tutorial - In-Depth Guide Part 1
The event loop doesn't have just one queue — it has (at least) two: the microtask queue and the macrotask (a.k.a. 'task') queue, and they are NOT processed with equal priority.
// Two distinct queues:
// 1. Microtask queue: Promise callbacks, queueMicrotask()
// 2. Macrotask queue: setTimeout, setInterval, I/O, UI eventsTwo Distinct Queues
2The Event Loop: Microtask/Macrotask Ordering | JavaScript Tutorial - In-Depth Guide Part 2
After each single macrotask finishes, the event loop fully drains the ENTIRE microtask queue before running the next macrotask — even if new microtasks are added while draining.
console.log('1: sync');
setTimeout(() => console.log('4: macrotask'), 0);
Promise.resolve().then(() => console.log('3: microtask'));
console.log('2: sync');
// Output order: 1, 2, 3, 4Microtasks Fully Drain First
3The Event Loop: Microtask/Macrotask Ordering | JavaScript Tutorial - In-Depth Guide Part 3
This explains a classic gotcha: setTimeout(fn, 0) does NOT run immediately after the current synchronous code — it waits for both the rest of the synchronous code AND the entire microtask queue to finish first.
setTimeout(() => console.log('macrotask'), 0);
Promise.resolve().then(() => console.log('microtask'));
// 'microtask' logs first, despite setTimeout appearing earlier in the code0ms setTimeout Isn't Instant
4The Event Loop: Microtask/Macrotask Ordering | JavaScript Tutorial - In-Depth Guide Part 4
async/await is built on Promises under the hood, so code after an 'await' resumes as a microtask — this is why interleaving async functions with setTimeout can produce surprising, non-obvious ordering.
async function example() {
console.log('1');
await null; // resumes as a microtask
console.log('3');
}
example();
console.log('2');
// Output: 1, 2, 3await Resumes as a Microtask
5The Event Loop: Microtask/Macrotask Ordering | JavaScript Tutorial - In-Depth Guide Part 5
Tracing execution order for mixed sync/microtask/macrotask code is a genuinely valuable debugging and interview skill — practice by mentally running the sync code first, then draining all microtasks, then processing one macrotask at a time.
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => {
console.log('C');
Promise.resolve().then(() => console.log('D'));
});
console.log('E');
// Order: A, E, C, D, BTracing Execution Order
6Step-by-Step Breakdown
The event loop doesn't have just one queue — it has (at least) two: the microtask queue and the macrotask (a.k.a. 'task') queue, and they are NOT processed with equal priority.
After each single macrotask finishes, the event loop fully drains the ENTIRE microtask queue before running the next macrotask — even if new microtasks are added while draining.
Checkpoint: Between two macrotasks, does the event loop run just one pending microtask, or the entire microtask queue?
- →The entire microtask queue, fully drained
- →Just one microtask at a time
This explains a classic gotcha: setTimeout(fn, 0) does NOT run immediately after the current synchronous code — it waits for both the rest of the synchronous code AND the entire microtask queue to finish first.
Checkpoint: If a setTimeout(fn, 0) and a Promise.resolve().then(fn2) are both scheduled in the same synchronous block, which one runs first?
- →The Promise's .then() callback, since it's a microtask
- →The setTimeout callback, since it's scheduled with 0ms
async/await is built on Promises under the hood, so code after an 'await' resumes as a microtask — this is why interleaving async functions with setTimeout can produce surprising, non-obvious ordering.
Tracing execution order for mixed sync/microtask/macrotask code is a genuinely valuable debugging and interview skill — practice by mentally running the sync code first, then draining all microtasks, then processing one macrotask at a time.
Next, we'll explore 'Microtasks'.
Level Up 🚀
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Understand Event Loop Ordering When Sequencing Focus Management After Async Updates
When moving focus programmatically after an async data update, knowing whether your focus-setting code runs as a microtask (right after the data resolves) or is deferred to a macrotask (like a setTimeout) affects whether focus lands correctly before or after the DOM has actually updated and is ready to receive it.
SEO Implications
- 1
No Direct SEO Effect
Event loop internals are a JavaScript execution-model concept; SEO relevance is limited to writing correct, non-janky async code that avoids blocking rendering.
Best Practices
Remember Promises Always Win a Race Against setTimeout(fn, 0)
This is a common source of confusion; internalizing that microtasks fully drain before any macrotask runs prevents incorrect assumptions about execution order in mixed async code.
Use queueMicrotask() Directly (Not a 0ms setTimeout) When You Specifically Need Microtask Timing
If your intent is "run this as soon as possible, after current synchronous code, but before any macrotask," queueMicrotask() expresses that intent directly rather than relying on Promise machinery as a workaround.
Frequent Bugs
Assuming `setTimeout(fn, 0)` runs before a Promise's `.then()` callback because it appears earlier in the source code or because the delay is 0, leading to incorrect assumptions about execution order.
Remember that all Promise-based microtasks fully drain before any macrotask (including a 0ms setTimeout) gets to run, regardless of the order they were scheduled in.
Writing an infinite microtask-producing loop (e.g. a .then() that always schedules another .then()), which can starve macrotasks (like rendering or user input) from ever running, since the microtask queue must fully drain first.
Ensure recursive or chained microtask scheduling has a genuine termination condition, or intentionally yield to a macrotask (e.g. via setTimeout) periodically if the work needs to continue indefinitely.
Real-World Examples
Debugging an Unexpected Console Log Order
A developer needed to explain to a teammate why a chain of Promise .then() calls all logged before a setTimeout(fn, 0) that was scheduled earlier in the same function.
setTimeout(() => console.log('timeout'), 0);
Promise.resolve()
.then(() => console.log('then 1'))
.then(() => console.log('then 2'));
// Output: then 1, then 2, timeout — the whole microtask chain drains first