Event Loop: Understanding JavaScript Execution Model

The event loop is the host environment scheduler around the JavaScript engine. A strong interview answer separates the call stack, task queues, microtask checkpoints, and rendering opportunities instead of memorizing Promise-before-setTimeout as a rule.
Quick Navigation: Mental Model • Actors in the Event Loop • Trace the Example • Drain the Microtasks • Run the Timer Tasks • Async/Await Is Promise Scheduling • Rendering and UI Performance • Browser vs Node.js
Mental Model
The Useful Browser Model
JavaScript execution is split between the language engine and the host environment. The engine runs JavaScript on a call stack. The browser schedules external work: timers, user events, network callbacks, microtasks, animation callbacks, and rendering.
A practical browser loop is:
run one task -> run JavaScript to completion -> drain microtasks -> maybe update rendering -> pick another taskThe word maybe matters. Browsers are not required to paint after every task. They render when the document needs it and the browser reaches a rendering opportunity.
The Interview Shortcut
For most browser output-order questions:
1. Run all synchronous code first.
2. Drain all microtasks in FIFO order.
3. Run one task, such as a timer callback.
4. Drain microtasks again.
5. Repeat.
Good engineers use this as a tracing model, not as a vague slogan.
Actors in the Event Loop
Call Stack
The call stack is where currently executing JavaScript runs. While the stack is busy, no timer callback, Promise callback, click handler, layout work, or paint can interrupt the current JavaScript frame.
Tasks
Tasks are scheduled units of host work. Examples include initial script execution, timer callbacks, user events, and message events. The event loop picks tasks one at a time.
Interviewers often say macrotask, but the HTML platform terminology is task.
Microtasks
Microtasks run after the current script or task exits and the call stack is empty, before the browser moves to the next task. Promise reactions, queueMicrotask, and MutationObserver callbacks use the microtask queue.
Rendering Opportunities
Rendering is coordinated by the browser around the event loop. Style, layout, paint, and compositing can be delayed by long JavaScript tasks or by a microtask queue that keeps refilling itself.
Trace the Example
Step 1: Run the Initial Script
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);Synchronous output:
1, 7Queues after the initial script:
Microtasks: [promise callback that logs 3, queueMicrotask callback that logs 6]
Tasks: [timer 2, timer 8]The zero-delay timers are not immediate. They are eligible to run later, after the current script and microtask checkpoint complete.
Drain the Microtasks
Step 2: First Microtask
The Promise callback runs first because it was queued before queueMicrotask(...6...).
It logs 3, queues timer 4, and queues a nested microtask 5.
Output: 1, 7, 3
Microtasks: [6, 5]
Tasks: [2, 8, 4]Step 3: Continue the Same Microtask Checkpoint
The browser does not leave the microtask checkpoint just because one microtask finished. It keeps draining the queue.
6 was already waiting before 5 was queued, so it runs next.
Output: 1, 7, 3, 6
Microtasks: [5]Then the nested Promise microtask runs:
Output: 1, 7, 3, 6, 5
Microtasks: []Nested microtasks do not jump ahead of older microtasks. They are appended to the microtask queue.
Run the Timer Tasks
Step 4: Tasks Run One at a Time
After the microtask queue is empty, the event loop can pick the next task. In this simplified browser interview case, the timers run in the order they were queued.
Tasks: [2, 8, 4]So the remaining output is:
2, 8, 4Final output:
1, 7, 3, 6, 5, 2, 8, 4Correct answer: B) 1, 7, 3, 6, 5, 2, 8, 4.
Async/Await Is Promise Scheduling
Await Splits Execution
async / await does not create a special third queue. Code before await runs synchronously until the await point. The continuation after the awaited value settles is scheduled through Promise machinery.
async function run() {
console.log('A');
await null;
console.log('B');
}
run();
console.log('C');Output:
A
C
BWhy: A runs during the current call stack. The continuation that logs B resumes asynchronously after the current synchronous script has finished.
Rendering and UI Performance
The Main Thread Is Shared
In the browser, JavaScript, style calculation, layout, paint coordination, and many input handlers compete for main-thread time. Long tasks delay everything behind them.
This is why event loop knowledge matters beyond output questions:
requestAnimationFrame callbacks are intended to run before a repaint, but they still need main-thread time.Microtasks Are Not Faster Tasks
Microtasks have scheduling priority, not magical performance. If code repeatedly queues microtasks, the browser can be prevented from reaching the next task or a healthy rendering opportunity.
function flood() {
queueMicrotask(flood);
}
flood();This is a scheduling bug, not a clever optimization.
Browser vs Node.js
Do Not Mix Runtime Rules
Browser event loop questions usually involve tasks, microtasks, rendering, timers, and DOM events. Node.js has different phases and Node-specific queues.
For example, process.nextTick is not a browser API and has special priority in Node.js.
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));In Node.js, nextTick runs before Promise microtasks:
nextTick
promiseInterview rule: ask which runtime is assumed when the snippet includes process.nextTick, setImmediate, DOM APIs, or rendering behavior.
Common Interview Mistakes
Mistake 1: Treating `setTimeout(fn, 0)` as Immediate
Zero delay means the callback can be scheduled after the current work. It still waits behind the current script and microtasks.
Mistake 2: Forgetting Microtask FIFO Order
A microtask queued from inside another microtask is appended. It does not automatically run before microtasks that were already queued.
Mistake 3: Saying Microtasks Always Run Before Everything
Microtasks run at microtask checkpoints. They do not interrupt currently executing JavaScript.
Mistake 4: Assuming Rendering Happens After Every Callback
The browser may render after microtasks when it reaches a rendering opportunity, but painting is conditional. If nothing needs rendering, or the browser chooses not to update yet, no paint occurs.
Mistake 5: Applying Browser Rules to Node.js
Node has runtime-specific phases and APIs. Browser output rules are not enough for process.nextTick or setImmediate.
How to Answer in Interviews
A Strong Answer Pattern
Say the model first, then trace the queues.
Synchronous code runs first. Promise reactions and queueMicrotask callbacks are microtasks. Timers are tasks. After the current script finishes, the microtask queue is drained in FIFO order, including microtasks queued during the drain. Only then does the event loop take the next task.Then show the state:
Sync output: 1, 7
Initial microtasks: 3, 6
Initial tasks: 2, 8
Microtask 3 queues: task 4 and microtask 5
Microtask order becomes: 6, 5
Task order becomes: 2, 8, 4
Final: 1, 7, 3, 6, 5, 2, 8, 4That answer demonstrates reasoning. It is better than reciting Promises-beat-setTimeout.