Predict the Output: Event Loop Order in 6 Snippets
Six short snippets with setTimeout, promises, await and queueMicrotask. Predict the order, run it, then read why. The one rule: sync code first, then all microtasks, then the next task.
Six short snippets. For each one, write down the order you expect before you look at the output. If you get all six right, you understand the event loop well enough for real work. If not, the explanation under each one tells you which rule you missed.
The only rule you need: run the current script to the end, then run all microtasks (promise callbacks, code after await, queueMicrotask), then run the next task (setTimeout, I/O, events), then all microtasks again.
All outputs below are from Node 24. Browsers give the same order for these snippets.
1. The classic
console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => console.log(3));
console.log(4);
1
4
3
2
1 and 4 are the script. 3 is a microtask. 2 is a task, so it waits for the microtasks.
2. Timer order
setTimeout(() => console.log("B, 10 ms"), 10);
setTimeout(() => console.log("A, 0 ms"), 0);
setTimeout(() => console.log("C, 0 ms"), 0);
A, 0 ms
C, 0 ms
B, 10 ms
Timers with the same delay run in the order they were set. The 10 ms timer was set first but becomes due later.
3. A microtask inside a task
setTimeout(() => {
console.log("task 1");
Promise.resolve().then(() => console.log("microtask from task 1"));
}, 0);
setTimeout(() => console.log("task 2"), 0);
task 1
microtask from task 1
task 2
After every task, the microtask queue is emptied before the next task. So the promise from task 1 runs before task 2, even though task 2 was queued first.
4. Chained thens vs a second promise
Promise.resolve()
.then(() => console.log("a1"))
.then(() => console.log("a2"));
Promise.resolve()
.then(() => console.log("b1"))
.then(() => console.log("b2"));
a1
b1
a2
b2
Only the first .then of each chain is queued at the start. a2 is queued when a1 finishes, which puts it behind b1. Two independent chains interleave.
5. Code before and after await
async function run() {
console.log("inside, before await");
await undefined;
console.log("inside, after await");
}
console.log("start");
run();
console.log("end");
start
inside, before await
end
inside, after await
An async function runs normally until its first await. Everything after it is a microtask, even when the awaited value is already there.
6. await inside a loop vs setTimeout
setTimeout(() => console.log("timeout"), 0);
async function loop() {
for (let i = 1; i <= 3; i++) {
await null;
console.log("loop", i);
}
}
loop();
loop 1
loop 2
loop 3
timeout
Each await queues a microtask, and each microtask queues the next one. The timer can't run until the microtask queue is empty, so the whole loop finishes first. If the loop awaited something real, like a fetch or a timer, the timeout could run in between.
⚠ Warning: This is also how you freeze a page with promises: a loop that keeps awaiting already-resolved values never gives tasks a turn. Clicks and repaints wait until it's done.
How did you do?
- Got 1 and 2 wrong: you're treating
setTimeout(..., 0)as "now". It means "after the script and all microtasks". - Got 3 or 4 wrong: microtasks are queued as each step finishes, not all at once at the start.
- Got 5 or 6 wrong: the code after
awaitis always a microtask.
💡 Tip: When real code runs in an order you don't expect, add numbered console.log lines and run it once. It takes a minute, and you stop guessing.
In snippet 6, what would print if you replaced await null with await new Promise(r => setTimeout(r, 0))? Write your guess, then run it.
About
Six snippets that test the event loop, from a plain setTimeout(0) to await inside a loop, with the real Node output and the reason for each. After this, you can predict the order instead of guessing.
- Responsible
- Christopher R
- Last Update
- 09/02/2026
- Reading Time
- 4 min
Reviews
Sign in to write a reviewComments (0)
Sign in to commentNo comments yet.