Event Loop: Understanding JavaScript Execution Model
The event loop coordinates synchronous execution, microtasks, and scheduled tasks. The most important interview rule is: run the current script, then drain all microtasks, then take the next task, and after that drain microtasks again before moving on.
Quick Navigation: Event Loop Mental Model • Step-by-Step Execution of This Code • Drain the Microtask Queue • Process the Timers • Why This Order Is Correct • Microtasks vs Tasks • Common Pitfalls • Browser vs Node.js
Event Loop Mental Model
Core Pieces
A practical browser mental model has these parts:
1. Call Stack - where synchronous JavaScript runs
2. Microtask Queue - high-priority async callbacks
3. Task Queue - lower-priority scheduled work like setTimeout
4. Event Loop - decides what runs next
Interview-Friendly Execution Order
1. Run current synchronous code to completion
2. Drain all microtasks
3. Run one task
4. Drain all microtasks again
5. RepeatCommon Examples
Microtasks
Promise.thenPromise.catchPromise.finallyqueueMicrotaskMutationObserverTasks
setTimeoutsetIntervalIn interviews, people often call these tasks macrotasks.
Step-by-Step Execution of This Code
Step 1: Run Synchronous Code
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => {
console.log('3');
setTimeout(() => console.log('4'), 0);
Promise.resolve().then(() => console.log('5'));
});
queueMicrotask(() => console.log('6'));
console.log('7');
setTimeout(() => console.log('8'), 0);What happens during the initial script?
console.log('1') → prints 1setTimeout(...2...) → queues timer 2Promise.resolve().then(...) → queues microtask 3queueMicrotask(...6...) → queues microtask 6console.log('7') → prints 7setTimeout(...8...) → queues timer 8At the end of synchronous code:
Output so far: 1, 7
Microtasks: [3-callback, 6-callback]
Tasks: [2-timer, 8-timer]Drain the Microtask Queue
Step 2: Process All Microtasks
First microtask: Promise callback
() => {
console.log('3');
setTimeout(() => console.log('4'), 0);
Promise.resolve().then(() => console.log('5'));
}This does three things:
345Queue state now:
Output: 1, 7, 3
Microtasks: [6-callback, 5-callback]
Tasks: [2-timer, 8-timer, 4-timer]Second microtask: queueMicrotask callback
Prints 6
Output: 1, 7, 3, 6
Microtasks: [5-callback]Third microtask: nested Promise callback
Prints 5
Output: 1, 7, 3, 6, 5
Microtasks: []
Tasks: [2-timer, 8-timer, 4-timer]Important Rule
Microtasks added while draining the microtask queue must also finish before the browser moves to the next task.
Process the Timers
Step 3: Run Tasks One by One
Now the microtask queue is empty, so the event loop takes the next queued timer.
First timer
console.log('2');Output becomes:
1, 7, 3, 6, 5, 2Second timer
console.log('8');Output becomes:
1, 7, 3, 6, 5, 2, 8Third timer
console.log('4');Final output:
1, 7, 3, 6, 5, 2, 8, 4Correct Answer
B) 1, 7, 3, 6, 5, 2, 8, 4
Why This Order Is Correct
Key Ordering Rules Used Here
Rule 1: Current script first
All synchronous code finishes before any microtask or timer callback runs.
Rule 2: Microtasks before next task
After the initial script finishes, all microtasks run before any setTimeout callback.
Rule 3: Nested microtasks still run immediately in the same drain phase
The Promise callback that logs 3 queues another microtask that logs 5. That nested microtask still runs before the event loop moves to timers.
Rule 4: Timer queued later runs later
Timer 4 is queued during the microtask phase, so it goes behind already-queued timers 2 and 8 in this simplified interview model.
Microtasks vs Tasks
Microtasks
These run after the current JavaScript stack is empty and before the next task is picked.
Examples:
Promise.thenPromise.catchPromise.finallyqueueMicrotaskMutationObserverTasks
These are picked one at a time by the event loop.
Examples:
setTimeoutsetIntervalSimple Example
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');Output:
1
4
3
2Because synchronous code finishes first, then microtasks, then timers.
Common Pitfalls
Pitfall 1: Thinking `setTimeout(fn, 0)` means immediate
console.log('1');
setTimeout(() => console.log('2'), 0);
console.log('3');Output:
1
3
2A zero-delay timer still waits until the current script and all pending microtasks are done.
Pitfall 2: Getting nested microtask order wrong
Promise.resolve().then(() => {
console.log('1');
Promise.resolve().then(() => console.log('2'));
});
queueMicrotask(() => console.log('3'));Output:
1
3
2Why?
12queueMicrotask(...3...) was already waiting in the queue3 runs before the newly added 2Pitfall 3: Assuming tasks run before microtasks
setTimeout(() => console.log('1'), 0);
Promise.resolve().then(() => console.log('2'));
setTimeout(() => console.log('3'), 0);Output:
2
1
3All microtasks run before the event loop moves to timers.
Pitfall 4: Overgeneralizing to every environment
The browser and Node.js event loops are not identical. Interview questions like this usually assume the browser mental model unless stated otherwise.
Browser vs Node.js
Browser
A simplified browser model is:
current script → microtasks → next task → microtasks → render opportunityNode.js
Node.js has additional event loop phases and special behavior such as process.nextTick().
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));In Node.js, nextTick runs before Promise microtasks.
So the output is:
nextTick
promiseInterview Advice
If a question includes process.nextTick, setImmediate, or other Node-specific APIs, do not apply the browser-only mental model blindly.
Rendering and the Event Loop
How This Relates to UI Performance
The event loop matters for rendering because long JavaScript tasks block the main thread.
If the main thread is busy:
Important Insight
Microtasks are high priority, but abusing them can also starve rendering if you keep queuing more and more work.
function flood() {
queueMicrotask(flood);
}
flood();This kind of pattern can prevent the browser from getting back to rendering work in a healthy way.
So 'microtasks are faster' does not mean 'always use microtasks everywhere'.
Interview Scenarios
Scenario 1: Why does Promise callback run before `setTimeout(..., 0)`?
Because Promise callbacks are microtasks, and microtasks are drained before the next timer task runs.
Scenario 2: Why does nested Promise callback run before timers?
Because it is queued into the microtask queue while the queue is still being drained.
Scenario 3: Why doesn't `setTimeout(..., 0)` mean zero waiting?
Because it only means the callback is eligible after the current script and microtasks finish, subject to the scheduler.
Scenario 4: What is the safest short interview explanation of the event loop?
JavaScript runs synchronous code first, then drains microtasks, then runs scheduled tasks one at a time, repeating this cycle.
Scenario 5: What is the answer to this problem?
B) 1, 7, 3, 6, 5, 2, 8, 4