# The JavaScript Event Loop, Step by Step

> Why does setTimeout(fn, 0) run after a Promise, and why does code after await come back later? Step through the call stack, the microtask queue and the macrotask queue one line at a time, and guess the output before you look.

- URL: https://www.maciejbednarczyk.com/en/blog/javascript-event-loop-step-by-step
- Author: Maciej Bednarczyk
- Language: en
- Published: 2026-11-08

This is a classic interview question. What does this print?

```javascript
console.log('A');

setTimeout(() => console.log('B'), 0);

Promise.resolve()
  .then(() => console.log('C'))
  .then(() => console.log('D'));

queueMicrotask(() => console.log('E'));

(async () => {
  console.log('F');
  await null;
  console.log('G');
})();

console.log('H');
```

If your first thought was "top to bottom, A to H", you are in good company. The code does run top to bottom. But some lines don't *run* their callback, they *schedule* it. To predict the order you need three things.

## The three pieces

-   **The call stack.** JavaScript has one thread. It runs whatever is on the stack and never interrupts it halfway. Your whole script is the first thing on it.
-   **The microtask queue.** `.then`, `.catch`, `.finally`, the code after `await` and `queueMicrotask` put callbacks here. Think of it as "right after the current code".
-   **The macrotask queue.** `setTimeout`, `setInterval`, DOM events, network and I/O callbacks put callbacks here. Think of it as "later".

And one rule ties them together:

> Run the synchronous code. When the stack is empty, run **every** microtask, including ones added along the way. Then take **one** macrotask. Repeat.

## Step through it

Type your guess first, then click through. Watch what lands in each queue, and when.

## Step through the event loop

Back and Next move one step at a time. The arrow keys work too.

### Promises, timers and await

```javascript
console.log('A');

setTimeout(() => console.log('B'), 0);

Promise.resolve()
  .then(() => console.log('C'))
  .then(() => console.log('D'));

queueMicrotask(() => console.log('E'));

(async () => {
  console.log('F');
  await null;
  console.log('G');
})();

console.log('H');
```

1.  The script starts. The whole file runs as one task on the call stack, top to bottom. Both queues are empty.
2.  A plain console.log runs right away. A is printed.
3.  setTimeout does not run its callback. It hands it to the browser's timer, and after 0 ms the callback lands in the macrotask queue. Nothing is printed.
4.  The promise is already resolved, so the first .then callback (C) is queued as a microtask. D is not queued yet: the second .then waits for C's promise, which settles only after C runs.
5.  queueMicrotask puts E straight into the microtask queue, behind C. Queues are first in, first out.
6.  This line calls the async function, so the engine steps inside it like any function call. Everything before the first await is synchronous: F is printed now.
7.  await pauses the function. The rest of it (G) is queued as a microtask, and the function returns to the caller. The script carries on.
8.  The last line prints H. That is the end of the synchronous code.
9.  The call stack is empty. Now the event loop drains the whole microtask queue, front first, before it touches any macrotask.
10.  C runs and prints C. Its promise settles, so only now does D join the queue - at the back, behind E and G, which have been waiting since the script ran.
11.  Next in line: E.
12.  Next: the async function resumes after await and prints G.
13.  Next: D. The microtask queue is empty now.
14.  Only with no microtasks left does the event loop take one macrotask: the timer callback prints B. setTimeout(fn, 0) means 'after everything else', not 'now'.
15.  Done: A F H C E G D B. Sync code first, then every microtask, then one macrotask - and the loop repeats.

Console: `A F H C E G D B`

## The three things that trip people up

### 1\. An async function starts synchronously

Calling an async function is a normal function call. Everything before the first `await` runs right away. That's why **F** comes second, before **H**. `await` splits the function in two: the part before it is synchronous, and the part after it behaves like a `.then` callback.

### 2\. A chained .then is not queued until its parent finishes

`.then(D)` waits on the promise returned by `.then(C)`. That promise only settles when C has run. So D joins the queue at that moment, at the back, behind E and G, which have been waiting since the script ran. Result: **C E G D**, not C D E G.

### 3\. setTimeout(fn, 0) does not mean "now"

The delay is a minimum, not a promise. Even with 0, the callback is a macrotask, so it waits for the synchronous code and for every microtask. **B** comes last.

Final answer: **A F H C E G D B**.

## Practice

Three more scenarios, from easy to sneaky. Guess first, then step through.

## Practice: guess, then step through

Pick a scenario. The last one catches a lot of people: a setTimeout after await.

### Promises, timers and await

```javascript
console.log('A');

setTimeout(() => console.log('B'), 0);

Promise.resolve()
  .then(() => console.log('C'))
  .then(() => console.log('D'));

queueMicrotask(() => console.log('E'));

(async () => {
  console.log('F');
  await null;
  console.log('G');
})();

console.log('H');
```

1.  The script starts. The whole file runs as one task on the call stack, top to bottom. Both queues are empty.
2.  A plain console.log runs right away. A is printed.
3.  setTimeout does not run its callback. It hands it to the browser's timer, and after 0 ms the callback lands in the macrotask queue. Nothing is printed.
4.  The promise is already resolved, so the first .then callback (C) is queued as a microtask. D is not queued yet: the second .then waits for C's promise, which settles only after C runs.
5.  queueMicrotask puts E straight into the microtask queue, behind C. Queues are first in, first out.
6.  This line calls the async function, so the engine steps inside it like any function call. Everything before the first await is synchronous: F is printed now.
7.  await pauses the function. The rest of it (G) is queued as a microtask, and the function returns to the caller. The script carries on.
8.  The last line prints H. That is the end of the synchronous code.
9.  The call stack is empty. Now the event loop drains the whole microtask queue, front first, before it touches any macrotask.
10.  C runs and prints C. Its promise settles, so only now does D join the queue - at the back, behind E and G, which have been waiting since the script ran.
11.  Next in line: E.
12.  Next: the async function resumes after await and prints G.
13.  Next: D. The microtask queue is empty now.
14.  Only with no microtasks left does the event loop take one macrotask: the timer callback prints B. setTimeout(fn, 0) means 'after everything else', not 'now'.
15.  Done: A F H C E G D B. Sync code first, then every microtask, then one macrotask - and the loop repeats.

Console: `A F H C E G D B`

### Warm-up: three lines

```javascript
setTimeout(() => console.log('1'), 0);
Promise.resolve().then(() => console.log('2'));
console.log('3');
```

1.  Three lines. Before you step through, guess the order.
2.  setTimeout schedules its callback as a macrotask. Nothing printed.
3.  .then on a resolved promise queues a microtask. Nothing printed.
4.  The only synchronous log: 3 is printed first, even though it is the last line.
5.  Stack empty, so microtasks run first: 2.
6.  Then one macrotask: 1. Result: 3 2 1.

Console: `3 2 1`

### Timers and promises inside async

```javascript
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));

(async () => {
  console.log('F');
  setTimeout(() => console.log('T'), 0);
  Promise.resolve().then(() => console.log('P'));
  await null;
  console.log('G');
})();

console.log('H');
```

1.  Being inside a function changes nothing. What matters is when the engine reaches each line.
2.  A is printed.
3.  B goes to the macrotask queue, C to the microtask queue.
4.  The function is called and runs synchronously up to its first await: F is printed.
5.  This setTimeout runs inside the function, but it is still just a registration: T joins the macrotask queue behind B.
6.  P joins the microtask queue behind C.
7.  await pauses the function; the rest (G) is queued as a microtask behind P.
8.  H is printed. Synchronous code is done.
9.  The microtask queue drains in order: C, P, G.
10.  Then the macrotasks, one per loop turn: B, then T. Result: A F H C P G B T.

Console: `A F H C P G B T`

### Trap: a timer after await

```javascript
console.log('1');
setTimeout(() => console.log('2'), 0);

(async () => {
  await null;
  setTimeout(() => console.log('3'), 0);
  console.log('4');
})();

Promise.resolve().then(() => console.log('5'));
console.log('6');
```

1.  A queue fills in the order the engine executes lines, not the order they are written. This one shows the difference.
2.  1 is printed; timer 2 is registered.
3.  The function hits await straight away. Everything after it - including the setTimeout for 3 - has not run yet. It waits as one microtask.
4.  5 joins the microtask queue behind the rest of the function.
5.  6 is printed. Synchronous code is done.
6.  The function resumes. Only now is the timer for 3 registered, so it lands behind 2. Then 4 is printed.
7.  Next microtask: 5.
8.  Macrotasks in queue order: 2, then 3. Result: 1 6 4 5 2 3.

Console: `1 6 4 5 2 3`

## The order, step by step

If an interviewer asks, walk through it in this order:

1.  **Synchronous code.** The script runs top to bottom on the call stack. Lines that schedule something (`setTimeout`, `.then`, `await`, `queueMicrotask`) only put a callback in a queue and move on.
2.  **Every microtask.** When the stack is empty, the event loop runs the whole microtask queue, front first: promise callbacks, the code after `await`, `queueMicrotask`. Microtasks added along the way run in the same round.
3.  **One macrotask.** Only then does it take a single macrotask: a timer, an event, an I/O callback.
4.  **Repeat.** After that one macrotask, it drains the microtask queue again, then takes the next macrotask, until both queues are empty.

Applied to the quiz:

1.  Synchronous: **A F H**
2.  Microtasks: **C E G D** (D joins only after C runs)
3.  Macrotask: **B**

One rule on top of that: a callback joins a queue when its line actually runs, not where it is written. In the final practice scenario, the `setTimeout` sits after an `await`, so it isn't even registered until the function resumes during the microtask phase. That's why its callback runs after a timer written further down the file.