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:
15234Why:
1and5are synchronous..then,.finally, next.thenare queued in promise chain order.finallyruns after previous settlement and before next chainedthen.
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:
17385Why:
- The
new Promise(...)executor runs immediately, so7is sync. .thencallback (8) is a microtask.setTimeoutcallback (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:
ABDCWhy:
foo()starts synchronously, soBlogs immediately.awaitpauses the function and schedules continuation (C) as a microtask.- Main thread continues with
Dfirst.
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:
1243Why:
await 10is effectivelyawait 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:
startendp1p2qmWhy:
- Initial microtask queue has
p1, thenp2. - While running
p1, we addqmto the end of microtask queue. - So
p2runs beforeqm.
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:
ADBCWhy:
- Both are microtasks.
- FIFO ordering decides sequence.
queueMicrotaskis scheduled first here, soBbeforeC.
7) return inside .then passes value to next link
Promise.resolve() .then(() => { console.log("A"); return "B"; }) .then((val) => { console.log(val); });
console.log("C");Output:
CABWhy:
- Outer sync
console.log("C")runs first. - First
.thenruns in microtask, returns"B". - Returned value resolves the next
.then, which logsB.
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:
13234Why:
- 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:
- What runs synchronously right now?
- Which callbacks are microtasks?
- Which callbacks are macrotasks?
- Did any running microtask enqueue more microtasks?
- 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.