CareerKit

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:

  1. 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.
  2. The microtask queue. Callbacks from promises: .then(), .catch(), .finally(), and the code after an await. These run as soon as the call stack is empty, and the queue is drained completely, including any new microtasks added along the way.
  3. 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");
Output
A
D
C
B

Why: 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");
Output
a
b
d
c

Why: 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");
Output
start
1
end
2

Why: 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");
Output
sync
then 1
then 2
timeout 1
timeout 2

Why: 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);
}
Output
var: 3
var: 3
var: 3
let: 0
let: 1
let: 2

Why: 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");
Output
1
6
8
4
7
2
3
5

Walk through it like this:

  1. Synchronous code: 1, then the async function prints 6 and pauses at await, then 8.
  2. Drain microtasks in the order they were queued: the .then() prints 4 (and schedules timer 5), then the code after await prints 7.
  3. First timer: prints 2 and queues a microtask.
  4. Drain microtasks before the next timer: 3.
  5. 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.