JS Sketchbooksee JavaScript think ✏️
← back to phase 9

9.6 — Node’s event loop & non-blocking I/O

6.2 promised this day would come: the event loop, backstage edition. In the browser, slow work parked in the “Web APIs” waiting room. In Node, the waiting room is a C workshop called libuv — and it’s how one thread reads a hundred files at once.

Nothing you learned changes — A, B, C still print in that order. What changes is that you finally meet the crew that made it possible, on the runtime your tests will live in.

watch it happen
import { readFile } from "node:fs/promises";

console.log("A — ask for the file");

const p = readFile("report.txt", "utf8");
p.then((text) => console.log("C —", text));

console.log("B — thread moves on");

// $ node loop.js
// A — ask for the file
// B — thread moves on
// C — suite: 12 passed, 1 failed

The machine you already own, in one breath: ONE thread, ONE call stack; anything slow parks OFF the stack in a waiting room; finished work queues up; the event loop delivers each callback when the stack is empty. That machine is identical in Node — only the waiting room changes.

the ONE stackglobalthe libuv workshop 🔧(no parked jobs)the queue(empty)⟳ the loop: stack empty? deliver next6.2’s machine, remembered: one thread, one stack; slow work parks OFF thestack; the loop feeds results back
under the hood

The deeper story, with the real names for things — this part is what turns “I saw it” into “I can explain it.”

Two honest wrinkles for later. First: some libuv errands (like file reads) actually use a small thread pool inside the workshop — but those threads are libuv’s C threads, not JavaScript. Your JS still runs on exactly one thread; the pool is plumbing.

Second: Node’s loop technically runs in phases (timers, I/O callbacks, and a Node-special setImmediate among them). The 6.2/6.5 model — sync first, then microtasks, then queued callbacks — predicts the right order for everything you’ll write in this course; the phase diagram is there when you someday need microscopic ordering.

Vocabulary you now own precisely: synchronous = runs to completion on the thread, in order. Asynchronous = parked now, delivered later via the queue. Blocking = holds the thread while waiting. Non-blocking I/O = the waiting happens in libuv, never on the thread.

Job note: this is why one Playwright process can drive several browser contexts in parallel without threads — every await is a parked job, and the loop interleaves them. When Phase 11 shows tests overlapping, you’ll recognize the workshop immediately.

your turn

⌨️ prove the order, in miniature

The sandbox’s runner captures async output — so stage the whole machine: synchronous prints, a parked timer, and a microtask, then predict-and-prove the delivery order.

requirements:

  • Print "A" synchronously.
  • Park a job: a setTimeout with delay 0 whose callback prints "C".
  • Queue a microtask: Promise.resolve().then(...) printing "P" (6.5’s fast lane).
  • Print "B" synchronously. Before running: write down the order you expect — then run and check yourself against the two-queues rule.

when you press RUN, the console must show exactly:

A
B
P
C

✏️ Quick check 1

console.log("A"); then readFile(...).then(() => console.log("C")); then console.log("B"). What order prints?

✏️ Quick check 2

In the browser the waiting room was the Web APIs. What is Node’s waiting room called?

✏️ Quick check 3

readFileSync in the middle of a busy server: does the thread keep serving other work while the disk answers? Type yes or no.

teach it back

🗣️ Now teach it back

Explain to a friend how Node reads 100 files with one thread: the loop, libuv, what non-blocking I/O means, and why readFileSync would ruin it.

Write it as if your friend is sitting next to you. Saved to your journal — future-you will use these notes to teach others.

a few sentences, minimum — you’ve got this
to remember
Same machine as 6.2 — one thread, one stack, queue, loop. Only the waiting room changed: libuv, a C workshop wired to the OS (disks, network, timers).
NON-BLOCKING I/O = the thread never stands in line; libuv queues the errand and the loop delivers the callback later. 100 parked jobs, one thread collecting — why Node conquered servers.
readFileSync = BLOCKING (the thread waits itself) — script-fine, server-fatal. All Phase 6 rules transfer: sync first, microtasks beat timers, and every Phase-11 await rides this exact loop.