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.
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 failedThe 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 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.
⌨️ 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
setTimeoutwith delay0whose 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:
✏️ 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.
🗣️ 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.