Node.js Streams: What They Are and How to Use Them
Introduction
Streams are Node.js’s built‑in abstraction for handling data that isn’t available all at once—such as files, network sockets, or HTTP request bodies. Unlike reading an entire file into memory, streams process data chunk by chunk, enabling efficient I/O and lower memory usage. This tutorial explains the four stream types, demonstrates common patterns, and shows how to handle backpressure and errors properly.
Step 1 – Understand the Stream Types
- Readable: emits data you can
.pipe()
out (e.g., file reads, HTTP responses). - Writable: accepts data you
.write()
in (e.g., file writes, HTTP requests). - Duplex: both readable and writable (e.g., TCP sockets).
- Transform: duplex that modifies data along the way (e.g.,
zlib.createGzip()
).
Step 2 – Reading a File as a Stream
Instead of fs.readFile()
, use fs.createReadStream()
to handle large files without exhausting memory:
const fs = require('fs');
const readStream = fs.createReadStream('large.txt', { encoding: 'utf8' });
readStream.on('data', chunk => {
console.log('Received', chunk.length, 'bytes');
});
readStream.on('end', () => console.log('File fully read'));
chunk is a Buffer (or string if encoding
is specified). Node reads the file in 64 KB blocks by default.
Step 3 – Piping a Readable into a Writable
The .pipe()
method connects streams and manages flow automatically:
const writeStream = fs.createWriteStream('copy.txt');
readStream.pipe(writeStream);
Node handles backpressure—pausing the source when the destination’s internal buffer is full and resuming when drained.
Step 4 – Transform Streams (Gzip Example)
Transform streams modify data mid-flight. Compress a file with Gzip:
const zlib = require('zlib');
const gzip = zlib.createGzip();
fs.createReadStream('large.txt')
.pipe(gzip)
.pipe(fs.createWriteStream('large.txt.gz'));
Step 5 – Handling Backpressure Manually
Sometimes you need explicit control. Check the return value of write()
and wait for the drain
event:
const writable = fs.createWriteStream('output.txt');
function writeLots() {
let ok = true;
let i = 0;
while (i < 1e6 && ok) {
ok = writable.write(`Line ${i++}\n`);
}
if (i < 1e6) {
writable.once('drain', writeLots);
}
}
writeLots();
drain signals that the buffer has flushed and writing can continue without data loss.
Step 6 – Robust Error Handling
Always attach error listeners to prevent uncaught exceptions:
readStream.on('error', console.error);
writeStream.on('error', console.error);
You can also use pipeline()
from stream/promises
to automatically handle cleanup on error.
Conclusion
Node.js streams enable efficient, non-blocking data handling—crucial for building scalable applications. By mastering the four stream types, piping, backpressure, and error handling, you can process large datasets, handle HTTP uploads/downloads, and chain complex transformations without overloading memory.