Event Loop: Understanding JavaScript Execution Model

Medium•

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.

Asked In

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. Repeat

Common Examples

Microtasks

•Promise.then
•Promise.catch
•Promise.finally
•queueMicrotask
•MutationObserver

Tasks

•setTimeout
•setInterval
•user input events
•network / message callbacks

In 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 1
•setTimeout(...2...) → queues timer 2
•Promise.resolve().then(...) → queues microtask 3
•queueMicrotask(...6...) → queues microtask 6
•console.log('7') → prints 7
•setTimeout(...8...) → queues timer 8

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

•prints 3
•queues timer 4
•queues microtask 5

Queue 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, 2

Second timer

console.log('8');

Output becomes:

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

Third timer

console.log('4');

Final output:

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

Correct 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.then
•Promise.catch
•Promise.finally
•queueMicrotask
•MutationObserver

Tasks

These are picked one at a time by the event loop.

Examples:

•setTimeout
•setInterval
•DOM events
•message events

Simple Example

console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');

Output:

1
4
3
2

Because 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
2

A 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
2

Why?

•first microtask logs 1
•it queues the nested microtask 2
•but queueMicrotask(...3...) was already waiting in the queue
•so 3 runs before the newly added 2

Pitfall 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
3

All 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 opportunity

Node.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
promise

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

•input can feel delayed
•rendering can be postponed
•animations can skip frames

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

Key Takeaways

1Synchronous code runs first on the call stack.
2After the current script finishes, the microtask queue is drained before the next task runs.
3Promise.then and queueMicrotask both schedule microtasks.
4setTimeout schedules a task, not immediate execution.
5Microtasks queued while microtasks are being drained still run before the next task.
6For this problem, the correct output is 1, 7, 3, 6, 5, 2, 8, 4.
7Nested microtasks do not automatically jump ahead of already queued microtasks.
8Browser and Node.js event loops have important differences.