Node.js Streams Cheat Sheet
Backpressure, pipeline and why you should stop using .pipe()
Streams process data without holding it in memory. They are also where Node applications leak, hang and lose errors — almost always for the same three reasons.
Types
| Type | Does | Example |
|---|---|---|
Readable
|
Produces data | fs.createReadStream, an HTTP request |
Writable
|
Consumes data | fs.createWriteStream, an HTTP response |
Duplex
|
Both, independently | A TCP socket |
Transform
|
Both, output derived from input | zlib.createGzip |
Composition
| Pattern | Note |
|---|---|
await pipeline(src, transform, dest)
|
The right default — propagates errors and cleans up |
src.pipe(dest)
|
Does NOT forward errors and leaks on failure |
for await (const chunk of readable)
|
Async iteration; honours backpressure automatically |
Readable.from(asyncGenerator())
|
Turn any async generator into a stream |
stream.destroy(err)
|
Tear down and propagate |
Backpressure
writable.write() returns false when its buffer is full. Ignoring that return value is how a fast reader and a slow writer turn into unbounded memory growth — the data has to go somewhere, and it goes into RAM. pipeline() and for-await both handle this for you; a hand-rolled loop must wait for the "drain" event itself.
Code examples
pipeline() with a transform
The composition that propagates errors and cleans up. .pipe() does neither.
import { pipeline } from 'node:stream/promises';
import { Transform } from 'node:stream';
import { createReadStream, createWriteStream } from 'node:fs';
import { createGzip } from 'node:zlib';
const upper = new Transform({
transform(chunk, _enc, cb) {
cb(null, chunk.toString().toUpperCase());
},
});
try {
await pipeline(
createReadStream('in.txt'),
upper,
createGzip(),
createWriteStream('out.txt.gz'),
);
console.log('done');
} catch (err) {
console.error('stream failed:', err); // reachable, unlike with .pipe()
}
Frequently asked questions
Why should I stop using .pipe()?
It does not forward errors and does not clean up on failure, so a broken destination leaks the source handle and your catch block never runs. pipeline() fixes both.
What is backpressure, practically?
write() returning false means the buffer is full. Ignoring it means the unread data piles up in memory. pipeline() and for-await both handle it; a hand-rolled loop must wait for "drain".
Was this cheat sheet useful?
Comments
No comments yet — be the first.
Need a different cheat sheet?
Tell us what you would like to see and we will build it — free.
Request a cheat sheet