Skip to main content
Logo
Overview
Rethinking the Event Loop: 8 Examples That Shift Your Perspective

Rethinking the Event Loop: 8 Examples That Shift Your Perspective

May 7, 2026
3 min read

Rethinking the Event Loop: 8 Examples That Shift Your Perspective

Most of us learn the JavaScript event loop in one line:

  • Call stack runs sync code first
  • Then microtasks (Promise.then, queueMicrotask)
  • Then macrotasks (setTimeout, setInterval, I/O callbacks)

That is correct, but not enough for real debugging.

These 8 examples are small, but each one fixes a common mental model mistake.

A quick personal note: in several frontend interview loops, I kept getting the same style of question: “guess the output” for async JavaScript snippets. At first, I treated them like trick questions. Over time, those rounds forced me to build a precise event loop mental model, and that changed how I debug async bugs in real projects too.


1) finally() ordering is still microtask-driven

console.log("1");
Promise.resolve()
.then(() => console.log("2"))
.finally(() => console.log("3"))
.then(() => console.log("4"));
console.log("5");

Output:

1
5
2
3
4

Why:

  • 1 and 5 are synchronous.
  • .then, .finally, next .then are queued in promise chain order.
  • finally runs after previous settlement and before next chained then.

2) Promise executor is synchronous, .then is not

console.log(1)
setTimeout(() => console.log(5), 0)
let promise = new Promise((resolve) => {
console.log(7)
resolve(8)
})
promise.then((value) => console.log(value))
console.log(3)

Output:

1
7
3
8
5

Why:

  • The new Promise(...) executor runs immediately, so 7 is sync.
  • .then callback (8) is a microtask.
  • setTimeout callback (5) is a macrotask.

3) Async/await trap: await splits execution

console.log("A");
async function foo() {
console.log("B");
await Promise.resolve();
console.log("C");
}
foo();
console.log("D");

Output:

A
B
D
C

Why:

  • foo() starts synchronously, so B logs immediately.
  • await pauses the function and schedules continuation (C) as a microtask.
  • Main thread continues with D first.

4) await with normal value still defers

console.log("1");
async function test() {
console.log("2");
await 10;
console.log("3");
}
test();
console.log("4");

Output:

1
2
4
3

Why:

  • await 10 is effectively await Promise.resolve(10).
  • Even non-promise values cause async function continuation in microtask phase.

5) Microtask created inside microtask

console.log("start");
Promise.resolve().then(() => {
console.log("p1");
queueMicrotask(() => {
console.log("qm");
});
});
Promise.resolve().then(() => {
console.log("p2");
});
console.log("end");

Output:

start
end
p1
p2
qm

Why:

  • Initial microtask queue has p1, then p2.
  • While running p1, we add qm to the end of microtask queue.
  • So p2 runs before qm.

6) queueMicrotask and promise callbacks share the same microtask queue

console.log("A");
queueMicrotask(() => console.log("B"));
Promise.resolve().then(() => console.log("C"));
console.log("D");

Output:

A
D
B
C

Why:

  • Both are microtasks.
  • FIFO ordering decides sequence.
  • queueMicrotask is scheduled first here, so B before C.

Promise.resolve()
.then(() => {
console.log("A");
return "B";
})
.then((val) => {
console.log(val);
});
console.log("C");

Output:

C
A
B

Why:

  • Outer sync console.log("C") runs first.
  • First .then runs in microtask, returns "B".
  • Returned value resolves the next .then, which logs B.

8) setTimeout(fn, 0) is not “run immediately”

console.log("1");
setTimeout(() => console.log("2"), 0);
for (let i = 0; i < 100000000; i++) {}
setTimeout(() => console.log("3"), 0);
setTimeout(() => console.log("4"), 0);
console.log("3");

Output:

1
3
2
3
4

Why:

  • Timers are macrotasks and can only run after current synchronous work finishes.
  • The heavy loop blocks the thread, delaying all timers.
  • Zero delay means “eligible soon,” not “instant.”

A practical model you can use while debugging

When output surprises you, check in this order:

  1. What runs synchronously right now?
  2. Which callbacks are microtasks?
  3. Which callbacks are macrotasks?
  4. Did any running microtask enqueue more microtasks?
  5. Is any long sync work delaying the next task turn?

If you follow this checklist, event loop questions become deterministic instead of guesswork.


Final takeaway

The event loop is less about memorizing definitions and more about understanding queue priority and enqueue timing.

Once that clicks, tricky async code starts feeling predictable.