How Does the Event Loop Order Microtasks and Macrotasks?
Learn exactly how the JavaScript event loop drains microtasks before macrotasks, with a traced console.log example.
Expected Interview Answer
After each single macrotask finishes executing, the event loop fully drains the entire microtask queue — including any new microtasks scheduled during that drain — before it renders or picks up the next macrotask, so Promise callbacks and queueMicrotask always run before the next setTimeout, I/O, or UI event.
JavaScript is single-threaded with one call stack, and the event loop coordinates that stack with two distinct task queues. Macrotasks include things like setTimeout/setInterval callbacks, I/O callbacks, and UI events; only one macrotask is processed per loop tick, taken from the front of the queue. Microtasks include Promise.then/catch/finally callbacks, queueMicrotask, and (in Node) process.nextTick (which actually runs even earlier than promise microtasks). Critically, after the currently running script or macrotask completes, the engine does not move to the next macrotask immediately — it drains the microtask queue completely first, and if a microtask schedules another microtask, that new one is also processed before returning control, meaning a runaway chain of microtasks can starve macrotasks and rendering. This is why “setTimeout(fn, 0)” scheduled before a "Promise.resolve().then(fn2)" still logs fn2 first — the microtask queue is emptied before the timer macrotask is even considered.
- Guarantees Promise chains resolve deterministically before any subsequent timer or I/O callback
- Explains why UI can appear to “freeze” if microtasks recursively reschedule themselves
- Lets you reason precisely about console.log ordering in interview whiteboard exercises
- Clarifies why process.nextTick in Node jumps the queue ahead of regular Promise microtasks
AI Mentor Explanation
The event loop is like an umpire who, after each single delivery (one macrotask) is bowled, first resolves every pending appeal and DRS review (the microtask queue) completely — even new appeals raised during review — before signaling for the next delivery to be bowled. A quick lbw appeal review always finishes before the next ball is queued, no matter how many follow-up reviews it spawns. Only once every appeal is cleared does play resume with the next delivery. That drain-all-appeals-before-next-ball rule is exactly how microtasks are exhausted before the next macrotask runs.
Step-by-Step Explanation
Step 1
Run the current script/macrotask to completion
The call stack executes synchronous code fully before anything else happens.
Step 2
Drain the entire microtask queue
Every Promise callback and queueMicrotask entry runs, including ones scheduled during this drain, until the queue is empty.
Step 3
Perform rendering (browser) if applicable
In browsers, the UI may repaint after microtasks drain and before the next macrotask.
Step 4
Pick the next macrotask
The event loop takes the next task (e.g., a setTimeout callback) from the macrotask queue and repeats the cycle.
What Interviewer Expects
- Correct claim that microtasks fully drain before the next macrotask, every cycle
- Ability to trace console.log ordering across setTimeout, Promise.then, and sync code
- Awareness that nested/chained microtasks keep draining before macrotasks resume
- Knowledge that process.nextTick (Node) runs before other microtasks
Common Mistakes
- Claiming setTimeout(fn, 0) runs before a Promise.then scheduled earlier
- Believing only one microtask runs per macrotask instead of the whole queue draining
- Confusing microtasks with macrotasks when explaining async/await ordering
- Not knowing a microtask that reschedules itself can starve rendering/macrotasks
Best Answer (HR Friendly)
“Think of JavaScript as finishing whatever it is currently doing, then always clearing out all the pending Promise-related work before it looks at the next timer or event. So even if you set a timer for zero milliseconds, any Promise that was already waiting will always run first, because the runtime empties that queue completely before moving on.”
Code Example
console.log('1: sync start')
setTimeout(() => console.log('2: macrotask (setTimeout)'), 0)
Promise.resolve().then(() => {
console.log('3: microtask A')
Promise.resolve().then(() => console.log('4: microtask A-nested'))
})
queueMicrotask(() => console.log('5: microtask B'))
console.log('6: sync end')
// Output order:
// 1: sync start
// 6: sync end
// 3: microtask A
// 5: microtask B
// 4: microtask A-nested <- scheduled during the drain, still runs before the timer
// 2: macrotask (setTimeout)Follow-up Questions
- How does process.nextTick in Node differ from Promise microtasks in priority?
- What happens to rendering if a microtask queue never empties?
- How does async/await map onto the microtask queue under the hood?
- Where do requestAnimationFrame callbacks fit relative to microtasks and macrotasks?
MCQ Practice
1. After a macrotask finishes, what does the event loop do next?
The microtask queue is fully emptied — including microtasks scheduled during the drain — before the loop proceeds.
2. Given setTimeout(fn, 0) scheduled before Promise.resolve().then(fn2), which logs first?
Microtasks (the Promise callback) always drain before the next macrotask (the timer callback) runs.
3. What can happen if a microtask keeps scheduling new microtasks indefinitely?
An endlessly self-rescheduling microtask chain blocks the loop from ever reaching macrotasks or repainting.
Flash Cards
Microtask examples? — Promise.then/catch/finally, queueMicrotask, process.nextTick (Node, highest priority).
Macrotask examples? — setTimeout, setInterval, I/O callbacks, UI events.
Order after a macrotask completes? — The full microtask queue drains, including newly queued ones, before the next macrotask.
Risk of runaway microtasks? — They can starve macrotasks and block rendering since the loop never proceeds until the queue is empty.