JavaScript Event Loop Explained with 6 Output Questions
Understand the call stack, microtasks and macrotasks, then test yourself on 6 event loop output questions that come up in JavaScript interviews.
"What will this code print?" is one of the most common JavaScript interview questions, and most of these questions test one idea: the event loop. Once you know the order in which JavaScript runs synchronous code, promise callbacks and timers, you can work out the answer instead of guessing.
This post explains the rules in plain words, then walks through six output questions. Every snippet below was run in Node.js 22, and the output shown is the real output.
The three rules you need
JavaScript runs your code on a single thread. While it runs, it keeps track of three things:
- The call stack. The code that is running right now. All synchronous code (plain function calls,
console.log, loops) runs here, top to bottom, until the stack is empty. - The microtask queue. Callbacks from promises:
.then(),.catch(),.finally(), and the code after anawait. These run as soon as the call stack is empty, and the queue is drained completely, including any new microtasks added along the way. - The task queue (macrotasks). Callbacks from
setTimeout,setInterval, I/O and UI events. The event loop takes one task, runs it, then drains the microtask queue again before taking the next task.
So the order is always: synchronous code first, then all microtasks, then one timer, then all microtasks again, and so on.
Question 1: setTimeout vs Promise
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");A
D
C
BWhy: A and D are synchronous, so they print first. The .then() callback is a microtask, so C runs as soon as the stack is empty. The timer callback is a macrotask, so B comes last, even though its delay is 0.
A delay of 0 does not mean "run now". It means "put this in the task queue as soon as possible", and the task queue waits for every microtask.
Question 2: The Promise executor is synchronous
console.log("a");
new Promise((resolve) => {
console.log("b");
resolve();
}).then(() => console.log("c"));
console.log("d");a
b
d
cWhy: the function you pass to new Promise() (the executor) runs immediately, on the call stack. Only the .then() callback is deferred. Many candidates expect b to print after d; it doesn't.
Question 3: async and await
async function run() {
console.log("1");
await null;
console.log("2");
}
console.log("start");
run();
console.log("end");start
1
end
2Why: an async function runs synchronously until its first await. So 1 prints straight away. The rest of the function (console.log("2")) is scheduled as a microtask, so end prints first and 2 after it.
Question 4: Timers scheduled inside microtasks
setTimeout(() => console.log("timeout 1"), 0);
Promise.resolve()
.then(() => {
console.log("then 1");
setTimeout(() => console.log("timeout 2"), 0);
})
.then(() => console.log("then 2"));
console.log("sync");sync
then 1
then 2
timeout 1
timeout 2Why: sync runs first. Then the microtask queue is drained: then 1 runs, and because it returns, the next .then() is queued as another microtask, so then 2 runs in the same drain. Only after that does the event loop take timers, in the order they were scheduled: timeout 1 was scheduled first, timeout 2 second.
Question 5: var vs let in a loop with setTimeout
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log("var:", i), 0);
}
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log("let:", j), 0);
}var: 3
var: 3
var: 3
let: 0
let: 1
let: 2Why: this one mixes the event loop with scope. The timers run after both loops have finished. With var, there is a single i for the whole function, and by then it is 3. With let, each loop iteration gets its own j, so each callback remembers its own value.
If an interviewer asks how to fix the var version without let, the classic answer is to capture the value in a new function scope, for example by passing i as the third argument to setTimeout, which forwards it to the callback.
Question 6: Everything together
console.log("1");
setTimeout(() => {
console.log("2");
Promise.resolve().then(() => console.log("3"));
}, 0);
Promise.resolve().then(() => {
console.log("4");
setTimeout(() => console.log("5"), 0);
});
(async () => {
console.log("6");
await undefined;
console.log("7");
})();
console.log("8");1
6
8
4
7
2
3
5Walk through it like this:
- Synchronous code:
1, then the async function prints6and pauses atawait, then8. - Drain microtasks in the order they were queued: the
.then()prints4(and schedules timer 5), then the code afterawaitprints7. - First timer: prints
2and queues a microtask. - Drain microtasks before the next timer:
3. - Next timer:
5.
How to answer these in an interview
Talk through your reasoning out loud. Interviewers care more about the method than the final list. A good structure is:
- "First, I'll list the synchronous logs in order."
- "Then the microtasks: promise callbacks and code after
await, in the order they were queued." - "Then each timer, and after each timer I drain any new microtasks."
If you get a question with process.nextTick (Node.js only), mention that its queue runs before the promise microtask queue. In browsers, requestAnimationFrame callbacks run before the next repaint, which is a separate step from both queues.
Practise more
Event loop questions are a small part of the output-question family. Scope, hoisting, closures, this, type coercion and object references are tested in exactly the same "what does this print?" format. You can read some answered examples for free in our JavaScript output questions page, and the JavaScript interview questions page covers the theory behind them.