Event Loop: Understanding JavaScript Execution Model
The JavaScript Event Loop manages execution order: current synchronous code runs to completion on the Call Stack, then all Microtasks (Promises, queueMicrotask) are drained, followed by one Task (macrotask like setTimeout), then microtasks again, before a rendering opportunity. This model explains why Promise callbacks run before setTimeout callbacks.
The Call Stack is the head chef cooking one dish at a time. Microtasks are urgent VIP orders that must be completed before any new regular orders (Tasks). The Event Loop is the manager who ensures the kitchen finishes all urgent work before starting the next regular order, and checks for rendering (UI updates) between cycles.
1Core Components
Call Stack: runs synchronous JS to completion. Microtask Queue: high priority (Promise.then, queueMicrotask). Task Queue (Macrotasks): lower priority (setTimeout, DOM events). Event Loop: coordinates the flow.
console.log('Sync');
Promise.resolve().then(() => console.log('Microtask'));
setTimeout(() => console.log('Task'), 0);2Execution Order Rules
1. Run current script fully. 2. Drain ALL microtasks. 3. Take ONE task from task queue. 4. Drain microtasks again. 5. Render opportunity. Repeat. Microtasks queued during draining still run before the next task.
3Step-by-Step Example
For the classic example with logs 1,7 + Promise + queueMicrotask + multiple setTimeouts, the output is: 1 7 3 6 5 2 8 4. Microtasks run before any timers, and nested microtasks are processed in the same drain phase.
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);4Microtasks vs Tasks
Microtasks (Promise.then, queueMicrotask, MutationObserver) run immediately after sync code and between tasks. Tasks (setTimeout, events, network callbacks) run one at a time with rendering opportunities in between.
| Property | Microtask | Task (Macrotask) |
|---|---|---|
| Timing | After current script / after each task | After all microtasks |
| Behavior | High priority, drained fully before next task | Lower priority, only one per loop cycle |
| Examples | Promise.then, queueMicrotask, MutationObserver | setTimeout, setInterval, DOM events |
| Use Case | DOM updates, state cleanup that must happen before render | Scheduled work, I/O callbacks, timers |
Microtask
Timing
After current script / after each task
Behavior
High priority, drained fully before next task
Examples
Promise.then, queueMicrotask, MutationObserver
Use Case
DOM updates, state cleanup that must happen before render
Task (Macrotask)
Timing
After all microtasks
Behavior
Lower priority, only one per loop cycle
Examples
setTimeout, setInterval, DOM events
Use Case
Scheduled work, I/O callbacks, timers
Common questions
- ›“Explain the JavaScript Event Loop.”
- ›“Why does Promise.then run before setTimeout(..., 0)?”
- ›“Walk through this code and predict the output order.”
- ›“What is the difference between microtasks and macrotasks?”
What interviewers look for
- Clear mental model: sync → microtasks → task → microtasks
- Understanding of nested microtask draining
- Knowledge of rendering impact and main-thread blocking
- Distinction between browser and Node.js behavior
Short answer (60 sec)
JavaScript runs synchronous code first, then drains the entire microtask queue (Promises, queueMicrotask), then processes one task (setTimeout etc.), drains microtasks again, and allows rendering. This repeats.
Detailed answer (senior level)
The full browser event loop is: current script → drain microtasks → one macrotask → drain microtasks → requestAnimationFrame callbacks → style/layout/paint/composite. Microtasks have higher priority and are fully exhausted before moving to the next task or render. This is why Promise callbacks always run before timers and why long tasks block the UI.
- Thinking setTimeout(fn, 0) runs immediately
- Forgetting that microtasks queued during draining still run before the next task
- Confusing microtasks with tasks (macrotasks)
- Assuming the same behavior in Node.js (process.nextTick is different)
- Not realizing long-running tasks block rendering
- ✓Synchronous code always runs first on the Call Stack
- ✓Microtasks are drained completely before any new task or rendering
- ✓setTimeout(0) does NOT mean immediate — it waits for microtasks
- ✓Nested microtasks still run in the same drain phase
- ✓Keep tasks short to avoid blocking the main thread and rendering