Event Loop Puzzles: Test Your Understanding
📝 Note: This post was written with the assistance of AI. The content reflects my personal learning and understanding and should not be taken as professional advice. Please do your own research and due diligence before acting on anything written here.
📚 Heads up: This post works best after reading the full series. If you haven't already, start with Post 1 and work your way through. The puzzles here assume you're familiar with the call stack, microtasks, macrotasks, Promises, async/await, and Node.js additions like
process.nextTick()andsetImmediate().
You've built the mental model. You've learned every player, every phase, every rule. Now it's time to put it all to the test.
This post has 10 progressively harder code puzzles — starting with browser basics and building up to a grand finale that combines every concept from the series. For each one, read the code carefully, predict the output, then click "Show Answer" to see the full breakdown.
Puzzles 1-6 are challenging. Puzzles 7-10 are brutal. No peeking. 🎯
🟢 Puzzle 1 — Browser Basics
console.log('A');
setTimeout(() => {
console.log('B');
}, 0);
Promise.resolve().then(() => {
console.log('C');
});
console.log('D');
What is the output? Write it down before revealing the answer.
Show Answer
Output:
A
D
C
B
Breakdown:
"A"— synchronous, runs immediatelysetTimeout(...)— registered as a macrotaskPromise.resolve().then(...)— registered as a microtask"D"— synchronous, runs immediately- Call stack empty — microtask queue drains →
"C" - Macrotask runs →
"B"
The rule in action: Synchronous first → microtasks → macrotasks. setTimeout(..., 0) still runs last because zero delay doesn't mean immediate — it means "after everything more urgent is done."
🟢 Puzzle 2 — Chained Promises
console.log('Start');
Promise.resolve()
.then(() => {
console.log('First');
})
.then(() => {
console.log('Second');
})
.then(() => {
console.log('Third');
});
console.log('End');
What is the output? Write it down before revealing the answer.
Show Answer
Output:
Start
End
First
Second
Third
Breakdown:
"Start"— synchronous, runs immediatelyPromise.resolve().then(...)— first.then()registered as microtask"End"— synchronous, runs immediately- Call stack empty — first microtask runs →
"First" - First
.then()completes — second.then()enters microtask queue →"Second" - Second
.then()completes — third.then()enters microtask queue →"Third"
The rule in action: Chained .then() calls don't all queue up at once. Each one only enters the microtask queue after the previous one finishes — sequential, not simultaneous.
🟡 Puzzle 3 — async/await Meets setTimeout
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
async function myFunc() {
console.log('3');
await Promise.resolve();
console.log('4');
}
myFunc();
console.log('5');
What is the output? Write it down before revealing the answer.
Show Answer
Output:
1
3
5
4
2
Breakdown:
"1"— synchronous, runs immediatelysetTimeout(...)— registered as a macrotaskmyFunc()is called synchronously — enters the function"3"— synchronous inside the function, runs immediatelyawait Promise.resolve()— pauses the function,"4"goes to microtask queue- JavaScript steps out of the function — back to main code
"5"— synchronous, runs immediately- Call stack empty — microtask runs →
"4" - Macrotask runs →
"2"
The rule in action: await pauses the function, not the whole program. JavaScript steps out and keeps running synchronous code. Only after the call stack is empty does it resume the paused function from the microtask queue.
🟡 Puzzle 4 — Microtasks Inside Macrotasks
console.log('Start');
setTimeout(() => {
console.log('Timeout');
Promise.resolve().then(() => {
console.log('Promise inside Timeout');
});
}, 0);
Promise.resolve().then(() => {
console.log('Promise');
});
console.log('End');
What is the output? Write it down before revealing the answer.
Show Answer
Output:
Start
End
Promise
Timeout
Promise inside Timeout
Breakdown:
"Start"— synchronous, runs immediatelysetTimeout(...)— registered as a macrotaskPromise.resolve().then(...)— registered as a microtask"End"— synchronous, runs immediately- Call stack empty — microtask drains →
"Promise" - Macrotask runs →
"Timeout" - Inside the macrotask, a new Promise is registered as a microtask
- After the macrotask completes — microtask queue drains again →
"Promise inside Timeout"
The rule in action: After every macrotask, JavaScript checks the microtask queue again before running the next macrotask. A Promise created inside a setTimeout callback still gets processed before any other macrotask.
🔴 Puzzle 5 — Node.js: process.nextTick vs Promises
console.log('A');
process.nextTick(() => {
console.log('B');
});
Promise.resolve().then(() => {
console.log('C');
});
setTimeout(() => {
console.log('D');
}, 0);
setImmediate(() => {
console.log('E');
});
console.log('F');
What is the output? Write it down before revealing the answer.
Show Answer
Output:
A
F
B
C
E
D
Breakdown:
"A"— synchronous, runs immediatelyprocess.nextTick(...)— registered as VIP microtaskPromise.resolve().then(...)— registered as regular microtasksetTimeout(...)— registered as macrotask, timers phasesetImmediate(...)— registered as macrotask, check phase"F"— synchronous, runs immediately- Call stack empty — VIP microtask runs first →
"B" - Regular microtask runs →
"C" - Outside I/O context —
setImmediatevssetTimeoutorder not guaranteed, but typicallysetImmediateruns first →"E" - Timers phase →
"D"
The rule in action: process.nextTick() sits above even Promises in the microtask queue. Outside an I/O callback, setImmediate typically runs before setTimeout — but this is not guaranteed. Never rely on that order outside I/O.
🔴 Puzzle 6 — Node.js Event Loop — I/O Meets Timers
const fs = require('fs');
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
setImmediate(() => {
console.log('3');
});
Promise.resolve().then(() => {
console.log('4');
});
process.nextTick(() => {
console.log('5');
});
fs.readFile(__filename, () => {
console.log('6');
process.nextTick(() => {
console.log('7');
});
Promise.resolve().then(() => {
console.log('8');
});
setImmediate(() => {
console.log('9');
});
setTimeout(() => {
console.log('10');
}, 0);
});
console.log('11');
What is the output? Write it down before revealing the answer.
Show Answer
Output:
1
11
5
4
3
6
7
8
9
2
10
Breakdown:
🔹 Synchronous code first:
"1"— synchronoussetTimeout(...)— macrotask, timers phasesetImmediate(...)— macrotask, check phasePromise.resolve().then(...)— microtaskprocess.nextTick(...)— VIP microtaskfs.readFile(...)— I/O handed to OS, callback queued when done"11"— synchronous
🔹 Microtasks drain:
process.nextTick()→"5"— VIP microtask goes firstPromise.then()→"4"— regular microtask goes next
🔹 Macrotasks — outside I/O:
setImmediate→"3"— check phasesetTimeout→"2"— timers phase
🔹 I/O callback runs:
"6"— synchronous inside I/O callback
🔹 Microtasks drain again inside I/O:
process.nextTick()→"7"— VIP microtaskPromise.then()→"8"— regular microtask
🔹 Macrotasks inside I/O:
setImmediate→"9"— check phase, right after I/OsetTimeout→"10"— timers phase, after check
The rule in action: Every rule from the series applied at once — synchronous first, then process.nextTick, then Promises, then I/O, then microtasks again inside I/O, then setImmediate, then setTimeout. The golden rule never broke once.
🔴🔴 Puzzle 7 — Multiple Awaits in Sequence
console.log('Start');
async function stepByStep() {
console.log('Step 1');
await Promise.resolve();
console.log('Step 2');
await Promise.resolve();
console.log('Step 3');
await Promise.resolve();
console.log('Step 4');
}
Promise.resolve().then(() => {
console.log('Outer Promise 1');
}).then(() => {
console.log('Outer Promise 2');
});
stepByStep();
console.log('End');
What is the output? Write it down before revealing the answer.
Show Answer
Output:
Start
Step 1
End
Outer Promise 1
Step 2
Outer Promise 2
Step 3
Step 4
Breakdown:
🔹 Synchronous code:
"Start"— synchronousPromise.resolve().then(...)— first outer.then()registered as microtaskstepByStep()called — enters function synchronously"Step 1"— synchronous inside functionawait Promise.resolve()— pauses after Step 1,"Step 2"queued as microtask- Back to main code →
"End"
🔹 Microtask queue — round 1:
- Outer Promise 1
.then()→"Outer Promise 1"— was registered before await resumed "Step 2"resumes from first await — runs, hits secondawait, queues"Step 3"as microtask- Outer Promise 2
.then()→"Outer Promise 2"— chained from Outer Promise 1
🔹 Microtask queue — round 2:
"Step 3"resumes — runs, hits thirdawait, queues"Step 4"as microtask
🔹 Microtask queue — round 3:
"Step 4"resumes — runs
The trap: Each await creates a new microtask checkpoint. Between each checkpoint, other microtasks already in the queue get a turn. The outer Promise chain interleaves with the async function's steps because they're competing in the same microtask queue.
🔴🔴 Puzzle 8 — setImmediate Inside and Outside I/O with Nested Microtasks
const fs = require('fs');
setImmediate(() => {
console.log('Immediate Outside');
process.nextTick(() => {
console.log('nextTick inside Immediate Outside');
});
Promise.resolve().then(() => {
console.log('Promise inside Immediate Outside');
});
});
fs.readFile(__filename, () => {
console.log('I/O callback');
setImmediate(() => {
console.log('Immediate Inside I/O');
process.nextTick(() => {
console.log('nextTick inside Immediate Inside');
});
Promise.resolve().then(() => {
console.log('Promise inside Immediate Inside');
});
});
setTimeout(() => {
console.log('Timeout Inside I/O');
}, 0);
});
console.log('Sync');
What is the output? Write it down before revealing the answer.
Show Answer
Output:
Sync
Immediate Outside
nextTick inside Immediate Outside
Promise inside Immediate Outside
I/O callback
Immediate Inside I/O
nextTick inside Immediate Inside
Promise inside Immediate Inside
Timeout Inside I/O
Breakdown:
🔹 Synchronous:
"Sync"— runs immediatelysetImmediate(...)outside — macrotask, check phasefs.readFile(...)— handed to OS
🔹 No microtasks queued yet — macrotask runs:
setImmediate Outside→"Immediate Outside"- Inside it:
process.nextTick(...)— VIP microtask,Promise.resolve().then(...)— microtask
🔹 Microtasks drain after the macrotask:
"nextTick inside Immediate Outside"— VIP microtask first"Promise inside Immediate Outside"— regular microtask next
🔹 I/O callback runs:
"I/O callback"— synchronous inside I/OsetImmediate(...)inside — check phase,setTimeout(...)inside — timers phase
🔹 Microtasks drain — none queued yet inside I/O
🔹 setImmediate inside I/O runs — guaranteed before setTimeout:
"Immediate Inside I/O"- Inside it:
nextTickandPromiseregistered
🔹 Microtasks drain after setImmediate:
"nextTick inside Immediate Inside"— VIP microtask"Promise inside Immediate Inside"— regular microtask
🔹 Timers phase:
"Timeout Inside I/O"
The trap: Microtasks drain after every macrotask — including after setImmediate. The nextTick and Promise inside setImmediate Outside don't run until that setImmediate callback completes.
🔴🔴 Puzzle 9 — Nested Microtasks: nextTick Inside Promise Inside nextTick
console.log('Start');
process.nextTick(() => {
console.log('nextTick 1');
Promise.resolve().then(() => {
console.log('Promise inside nextTick 1');
process.nextTick(() => {
console.log('nextTick inside Promise inside nextTick 1');
});
});
process.nextTick(() => {
console.log('nextTick 2 — queued inside nextTick 1');
});
});
Promise.resolve().then(() => {
console.log('Outer Promise');
process.nextTick(() => {
console.log('nextTick inside Outer Promise');
});
});
setTimeout(() => {
console.log('Timeout');
}, 0);
console.log('End');
What is the output? Write it down before revealing the answer.
Show Answer
Output:
Start
End
nextTick 1
nextTick 2 — queued inside nextTick 1
Outer Promise
nextTick inside Outer Promise
Promise inside nextTick 1
nextTick inside Promise inside nextTick 1
Timeout
Breakdown:
🔹 Synchronous:
"Start","End"
🔹 VIP microtask queue — nextTick 1 runs:
"nextTick 1"- Inside it: a Promise microtask queued, and a new
nextTickqueued - New
nextTickgoes to VIP queue immediately
🔹 VIP microtask queue — nextTick 2 runs next (still VIP priority):
"nextTick 2 — queued inside nextTick 1"
🔹 VIP queue empty — regular microtask queue drains:
- Outer Promise →
"Outer Promise" - Inside it: a new
nextTickis queued — VIP microtask jumps ahead "nextTick inside Outer Promise"— runs immediately before remaining microtasks
🔹 Back to regular microtask queue:
"Promise inside nextTick 1"— was queued inside nextTick 1- Inside it: a new
nextTickqueued — VIP jumps ahead again "nextTick inside Promise inside nextTick 1"
🔹 Macrotask:
"Timeout"
The trap: process.nextTick() called inside any callback — even inside a Promise .then() — immediately jumps to the front of the microtask queue above any remaining Promises. The microtask queue grows dynamically and drains completely before any macrotask runs.
🔴🔴🔴 Puzzle 10 — Grand Finale: Everything Combined
const fs = require('fs');
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
process.nextTick(() => {
console.log('4');
});
});
}, 0);
setImmediate(() => {
console.log('5');
});
Promise.resolve()
.then(() => {
console.log('6');
return Promise.resolve();
})
.then(() => {
console.log('7');
});
process.nextTick(() => {
console.log('8');
process.nextTick(() => {
console.log('9');
});
});
async function asyncFunc() {
console.log('10');
await null;
console.log('11');
await Promise.resolve();
console.log('12');
}
asyncFunc();
fs.readFile(__filename, () => {
console.log('13');
process.nextTick(() => {
console.log('14');
});
Promise.resolve().then(() => {
console.log('15');
});
setImmediate(() => {
console.log('16');
process.nextTick(() => {
console.log('17');
});
});
setTimeout(() => {
console.log('18');
}, 0);
});
console.log('19');
What is the output? Write it down before revealing the answer.
Show Answer
Output:
1
10
19
8
9
6
11
7
12
5
2
13
14
15
16
17
18
3
4
Breakdown:
🔹 Synchronous code:
"1"— syncsetTimeout(...)— macrotask, timers phasesetImmediate(...)— macrotask, check phasePromise.resolve().then(...)— first outer Promise microtask queuedprocess.nextTick(...)— VIP microtask queuedasyncFunc()called — enters function"10"— sync inside functionawait null— pauses,"11"queued as microtask- Back to main code →
"19"
🔹 VIP microtasks first:
"8"— nextTick runs, queues another nextTick inside"9"— nested nextTick runs immediately after (VIP queue drains fully)
🔹 Regular microtasks:
- Outer Promise first
.then()→"6"— returnsPromise.resolve(), adding extra microtask checkpoint await nullresumes →"11"— hits second await, queues"12"- Outer Promise second
.then()→"7"— extra checkpoint resolves "12"— second await resumes
🔹 Macrotasks — outside I/O (order: setImmediate before setTimeout):
setImmediate→"5"setTimeout→"2", inside it Promise queued → microtasks drain immediately after"3"— Promise inside setTimeout"4"— nextTick inside that Promise (VIP, runs before any remaining microtasks)
🔹 I/O callback runs:
"13"— sync- nextTick and Promise registered inside I/O
🔹 Microtasks inside I/O:
"14"— nextTick (VIP)"15"— Promise
🔹 setImmediate inside I/O — guaranteed before setTimeout:
"16"— setImmediate runs- Inside: nextTick queued → microtasks drain
"17"— nextTick inside setImmediate
🔹 Timers inside I/O:
"18"— setTimeout
The traps in this puzzle:
- Returning
Promise.resolve()from a.then()adds an extra microtask checkpoint —"7"gets delayed by one extra tick process.nextTick()inside a Promise.then()jumps ahead of remaining Promises immediately- Each
awaitis a separate microtask checkpoint — other queued microtasks interleave between awaits - After every macrotask — including
setTimeout— the microtask queue drains completely before the next macrotask
The rule in action: Every single rule from the entire series applied simultaneously. The golden rule never broke once — not inside I/O, not inside macrotasks, not inside nested microtasks.
✅ How Did You Do?
| Puzzles Correct | What It Means |
|---|---|
| 1-2 | Good start — revisit Posts 1 and 2 |
| 3-4 | Solid foundation — revisit Posts 2 and 3 for async/await specifics |
| 5-6 | Strong understanding — revisit Posts 3 and 4 for Node.js specifics |
| 7-8 | Advanced level — one more careful pass through the full series |
| 9 | Near mastery — the dynamic microtask queue is the last frontier |
| 10 | Event loop mastery — you're ready for real world async JavaScript at any level |
🧠 The Rules That Never Break
No matter how complex the code gets, these rules always hold:
- Synchronous code always runs first — no exceptions
process.nextTick()always runs before Promises — Node.js onlyprocess.nextTick()queued inside any callback always jumps to the front of the microtask queue immediately- Microtasks always drain completely before any macrotask — no exceptions
- After every macrotask, microtasks drain again before the next macrotask
- Returning
Promise.resolve()from a.then()adds an extra microtask checkpoint - Each
awaitis a separate microtask checkpoint — other microtasks can interleave between them - Inside an I/O callback —
setImmediatealways beatssetTimeout - Outside an I/O callback — never rely on the order of
setImmediatevssetTimeout
You started this series not knowing what a call stack was. You just decoded a 20-line puzzle combining every async tool JavaScript and Node.js have to offer. That's not a small thing.
This is Part 5 of a 5-part series on the JavaScript Event Loop.

