# Event Loop — Part 2: Ordering With Multiple Queues

(Full depth on microtasks vs macrotasks is its own topic — this section focuses specifically on how the event loop sequences them.)

## 1. Two Queues, Not One

The event loop actually checks **two separate queues**, with strict priority: the **microtask queue** (Promise callbacks, `queueMicrotask`) is always fully drained before the loop touches the **macrotask queue** (`setTimeout`, `setInterval`, I/O callbacks) again.

```js
console.log("1: sync");

setTimeout(() => console.log("4: macrotask"), 0);

Promise.resolve().then(() => console.log("3: microtask"));

console.log("2: sync");
```

Output: `1`, `2`, `3`, `4` — the microtask (Promise callback) always runs before the macrotask (setTimeout), even though the `setTimeout` line appeared first in the source.

## 2. The Precise Loop Algorithm

```
loop forever:
  1. Run the call stack until empty (all currently synchronous code).
  2. Drain the ENTIRE microtask queue — run every microtask, even
     new ones added WHILE draining (they still get processed
     before moving on).
  3. Pick exactly ONE macrotask from the macrotask queue, run it
     (which itself may add more microtasks — go back to step 2
     immediately after).
  4. (In browsers) possibly re-render the page.
  5. Repeat from step 1.
```

The critical detail: microtasks are drained **completely**, including any new microtasks scheduled by earlier ones, before even a single macrotask is allowed to run.

## 3. Microtasks Can Starve Macrotasks

```js
function scheduleForever() {
  Promise.resolve().then(scheduleForever); // keeps adding a new microtask, forever
}
scheduleForever();
setTimeout(() => console.log("this may never run"), 0); // starved — the microtask queue never empties
```

Because the loop must fully drain the microtask queue before touching a macrotask, an infinitely self-scheduling microtask chain can starve all timers and I/O indefinitely — a real (if uncommon) production bug pattern.

## 4. A Complete Worked Example

```js
console.log("A");

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

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

setTimeout(() => console.log("E"), 0);

Promise.resolve().then(() => console.log("F"));

console.log("G");
```

Trace:
1. Synchronous: `A`, `G` print immediately.
2. Stack empty → drain microtasks: `C` runs, schedules the next `.then` (`D`) as a NEW microtask; `F` runs (was already queued); then `D` runs (queued during step 2, but the queue isn't considered "empty" until it truly has nothing left).
3. Macrotask queue: `B`, then `E` (in the order their timers were registered).

Final order: `A`, `G`, `C`, `F`, `D`, `B`, `E`.

## Under the Hood: Node.js's Real Event Loop Has Six Named Phases

The simplified "one macrotask queue" model from section 2 is accurate enough for reasoning about ordering in most code, but Node's actual libuv-based event loop is more structured: it cycles through **six distinct phases**, each with its own queue, in a fixed order, every single iteration.

```
   ┌───────────────────────────┐
┌─▶│           timers            │  setTimeout / setInterval callbacks whose time has come
│  └─────────────┬─────────────┘
│  ┌─────────────▼─────────────┐
│  │      pending callbacks      │  some system-level callbacks deferred from the previous cycle
│  └─────────────┬─────────────┘
│  ┌─────────────▼─────────────┐
│  │       idle, prepare          │  internal use only
│  └─────────────┬─────────────┘
│  ┌─────────────▼─────────────┐
│  │            poll              │  retrieve new I/O events; execute I/O-related callbacks
│  └─────────────┬─────────────┘
│  ┌─────────────▼─────────────┐
│  │            check             │  setImmediate() callbacks specifically
│  └─────────────┬─────────────┘
│  ┌─────────────▼─────────────┐
│  │        close callbacks        │  e.g. socket.on('close', ...)
│  └─────────────┬─────────────┘
└────────────────┘
```

**Microtasks (Promise callbacks, `queueMicrotask`) and `process.nextTick` are drained completely between every single phase transition shown above** — not just once per full loop iteration. This is a genuinely important refinement of the simplified model: it's not "sync → all microtasks → one macrotask → repeat," it's "sync → all microtasks → timers phase → all microtasks → pending callbacks phase → all microtasks → ... " and so on through all six phases, every cycle.

### Why `setImmediate` and `setTimeout(fn, 0)` have a famously unreliable relative order

```js
setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));
```

`setTimeout(fn, 0)` callbacks fire in the **timers** phase; `setImmediate` callbacks fire in the **check** phase — two different phases, several phases apart in the cycle above. Which one logs first depends on how long it took to reach this point from process start (a timer's minimum delay is measured from scheduling, and sub-millisecond timing jitter can flip which phase the loop happens to be in first) — this is *why* this specific ordering question is infamous for being non-deterministic at the top level of a Node script, while being **deterministic** inside an I/O callback (where `setImmediate` reliably fires before a `setTimeout(fn, 0)` scheduled in the same callback, because the loop is already past the timers phase for that cycle and will hit the check phase before timers comes around again).

## 5. Common Pitfalls

1. **Assuming `setTimeout(fn, 0)` beats a Promise `.then()`** — it never does; ALL pending microtasks run first, unconditionally.
2. **Chained `.then()` calls "jumping the queue" unexpectedly** — each `.then()` adds a NEW microtask when its predecessor resolves; a long `.then()` chain can interleave with other already-queued microtasks in ways that surprise people expecting strict FIFO across unrelated chains.
3. **Not realizing rendering (in browsers) happens only between macrotasks**, not between every microtask — a runaway microtask chain can also block visual updates, not just other timers.
4. **Debugging "impossible" ordering** without accounting for the fact that microtasks scheduled DURING microtask draining still get processed in the same pass.

## 6. Quick Reference

```
sync code           → runs first, always, completely
microtasks           → ALL drained next, including newly-added ones, before any macrotask
one macrotask         → runs, may add more microtasks → drain those → repeat
Promise.then          → microtask
setTimeout/setInterval → macrotask
```
