The event loop in depth
Node is single-threaded for your JavaScript code: a single stack runs one thing at a time. Concurrency comes from the event loop, which decides what runs next when the stack becomes empty. Not all pending tasks are equal:
- Microtasks (microtasks): the continuations of promises (
.then, the code after anawait) andqueueMicrotask. They are drained entirely as soon as the current stack finishes, before moving on to the next macrotask. - Macrotasks (macrotasks):
setTimeout,setInterval, I/O. They are processed one per iteration of the loop, after draining all the microtasks.
That is why an already-resolved promise "wins" over a setTimeout(…, 0):
console.log("1: synchronous");
setTimeout(() => console.log("4: timeout (macrotask)"), 0);
Promise.resolve().then(() => console.log("3: promise (microtask)"));
console.log("2: synchronous");
// Actual order: 1, 2, 3, 4
First all the synchronous code (1, 2). When the stack is emptied, the loop drains the microtasks (3). Only then does it take the next macrotask (4).
Streams and backpressure
Loading a 2 GB file into memory with readFile blows it up. A stream
processes data in chunks (chunks), with constant memory usage:
import { createReadStream, createWriteStream } from "node:fs";
createReadStream("input.bin").pipe(createWriteStream("output.bin"));
What if the destination is slower than the source (a slow disk, a congested network)?
That is where backpressure comes in: the write stream signals that
its buffer is full (write() returns false), and the read one pauses until
it is emptied (drain event). pipe() handles this for you automatically, preventing
memory from growing out of control.
Buffer: binary data
Text strings are not enough for images, audio or binary protocols. A
Buffer is a region of memory of raw bytes (integers from 0 to 255):
const buf = Buffer.from("AB");
console.log(buf.length); // 2 bytes
console.log(buf[0]); // 65 (code of the character 'A')
console.log(buf.toString("hex")); // "4142"
The chunks that travel through a stream are, in fact, Buffers.
Examples
Microtask (promise) before macrotask (timeout)
console.log("A");
setTimeout(() => console.log("D"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("B");
// Prints: A, B, C, D