Event Loop: Understanding JavaScript Execution Model

Medium•
Comic-style Event Loop hero showing call stack, microtasks, task queue, render checkpoint, and repeat flow.

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.

Asked In

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 task

The 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, 7

Queues 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, 4

Final output:

1, 7, 3, 6, 5, 2, 8, 4

Correct 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
B

Why: 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:

•A long JavaScript task can delay input handling.
•A large microtask chain can postpone rendering opportunities.
•Too much Promise-based work can still make the UI feel stuck.
•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
promise

Interview 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, 4

That answer demonstrates reasoning. It is better than reciting Promises-beat-setTimeout.

Key Takeaways

1The event loop is host scheduling around the JavaScript engine, not a feature of Promise syntax alone.
2Synchronous JavaScript runs to completion before queued callbacks run.
3Promise reactions and queueMicrotask callbacks are microtasks.
4Timers, user events, and message events are tasks; macrotask is common interview shorthand.
5Microtasks drain in FIFO order, including microtasks queued during the drain.
6Nested microtasks are appended; they do not skip older queued microtasks.
7Rendering happens at browser rendering opportunities, not necessarily after every task.
8For this snippet, the correct output is 1, 7, 3, 6, 5, 2, 8, 4.
9Browser and Node.js event loop rules are different; ask which runtime is assumed.