Node Essentials
The event loop & async I/O
Lesson 2 of 5
What you'll learn
- Understand why blocking is the enemy in a single-threaded runtime
- Order synchronous code, microtasks, and timers correctly
- See that promises run before timers within a tick
Node runs your JavaScript on a single thread. If that thread stops to wait for a disk read or a network response, nothing else can happen. So Node never waits: it hands slow I/O off to the system, keeps running your code, and comes back when the result is ready. That coming-back is the event loop.
const fs = require("node:fs");
console.log("1: start");
fs.readFile("data.txt", () => console.log("3: file read"));
console.log("2: end");
// prints 1, 2, then 3 — the read finished later
Microtasks vs. macrotasks
Not all "later" is equal. After each chunk of synchronous code finishes, Node drains the microtask queue (resolved promises, queueMicrotask) completely before touching the macrotask queue (timers like setTimeout).
console.log("sync");
setTimeout(() => console.log("timer"), 0);
Promise.resolve().then(() => console.log("microtask"));
// order: sync, microtask, timer
The synchronous line runs first. Then the promise callback — even though the timer was 0ms — because microtasks always drain before the loop moves on to timers.
Don't block the loop
A long for loop or a synchronous fs.readFileSync freezes the entire process — every pending request stalls until it finishes. Keep per-tick work small and prefer the async APIs.
The challenge is a model event loop: a sync phase, a microtask queue that drains fully, then a timer queue.
Run it. The loop logs sync work, then drains all microtasks, then runs timers — matching Node's real ordering.
Next: reading and writing files with paths that work on any OS.
Sign in to save your progress across devices.