stream/promises.pipeline
stream/promises.pipeline connects Readable → Transform → Writable with correct error propagation, stream destruction on failure, and a Promise API suited to async/await handlers.
Recipe
import { pipeline } from 'node:stream/promises';
import { createReadStream, createWriteStream } from 'node:fs';
import { createGzip } from 'node:zlib';
await pipeline(
createReadStream('input.log'),
createGzip(),
createWriteStream('input.log.gz'),
);When to reach for this:
- Any multi-stage file or HTTP processing chain
- Replacing
.on('error')spaghetti on manual pipes - Express/Fastify download endpoints streaming files
- ETL scripts run via cron or worker processes
Working Example
import { pipeline } from 'node:stream/promises';
import { createReadStream } from 'node:fs';
import { createServer } from 'node:http';
import { Transform } from 'node:stream';
import { createGzip } from 'node:zlib';
const lineCount = new Transform({
transform(chunk, _enc, cb) {
const lines = String(chunk).split('\n').length - 1;
(this as Transform & { lines?: number }).lines =
((this as Transform & { lines?: number }).lines ?? 0) + lines;
cb(null, chunk);
},
});
const server = createServer(async (req, res) => {
if (req.url !== '/export') {
res.writeHead(404).end();
return;
}
try {
res.writeHead(200, {
'content-type': 'application/gzip',
'content-disposition': 'attachment; filename="export.log.gz"',
});
await pipeline(
createReadStream('app.log'),
lineCount,
createGzip(),
res,
);
console.log('lines processed', (lineCount as Transform & { lines?: number }).lines);
} catch (err) {
if (!res.headersSent) res.writeHead(500);
res.end();
console.error('pipeline failed', err);
}
});
server.listen(3000);What this demonstrates:
pipelineaccepts Writable HTTPresas final sink- Errors after
headersSentrequire ending response without second status line - Intermediate Transform can accumulate metrics while passing bytes through
awaitintegrates with try/catch like any async I/O
Deep Dive
How It Works
- On error,
pipelinedestroys participating streams with the error. - On completion, streams are closed in order - Writable gets
end. - Supports AbortSignal in Node 24 for cancellation mid-flight.
- Callback version exists in
node:stream- Promise variant preferred in async code.
pipeline vs pipe
| Feature | pipeline | manual pipe |
|---|---|---|
| Error forward | Yes | Manual |
| Destroy on fail | Yes | Manual |
| Promise API | Yes | No |
| AbortSignal | Yes | Manual |
TypeScript Notes
import { pipeline } from 'node:stream/promises';
import type { Readable, Writable } from 'node:stream';
export async function safePump(
source: Readable,
sink: Writable,
signal?: AbortSignal,
): Promise<void> {
await pipeline(source, sink, { signal });
}Gotchas
- Not catching pipeline rejection - unhandled rejection crashes process. Fix: try/catch in route handlers.
- Headers after piping started - must
writeHeadbeforepipelineto sink response. Fix: set headers first. - Client abort mid-stream - treat as non-fatal; listen
req.on('aborted')optionally. Fix: passAbortSignallinked to request. - Mixing pipeline and manual write to same Writable - corrupted output. Fix: one pipeline per response.
- Transform not flushing partial state - implement
_flushfor trailing buffered data.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
finished + manual destroy | Legacy streams code | Greenfield |
pump (npm) | Older Node versions | Node 24 has native pipeline |
| Buffer entire payload | Tiny files < 1 MB | Large downloads |
Web pipeThrough | fetch Web Streams | node:fs sources |
FAQs
Does pipeline close all streams?
Yes - on success or failure participating streams are destroyed/ended appropriately.
Can pipeline take more than three streams?
Yes - variadic arguments: pipeline(a, b, c, d, sink).
How to cancel pipeline?
Pass { signal: abortController.signal } option in Node 24 pipeline.
What errors should I expect?
ENOENT on missing file, ECONNRESET on client disconnect, zlib errors on corrupt gzip.
Can final argument be a function?
Legacy callback API - Promise version preferred in async handlers.
Does order matter?
Yes - data flows left to right through each Transform.
pipeline with object mode?
All streams in chain must agree on object vs byte mode (with converting Transform).
How to log bytes transferred?
PassThrough tap stream in middle counting chunk.length.
Express 5 streaming?
Same pattern - await pipeline(source, res) inside async route with error wrapper.
Fastify streaming reply?
Use reply.send(stream) or pipeline into raw response per Fastify 5 docs for fine control.
Testing pipeline?
Use stream/consumers.buffer or Writable that collects chunks in memory for assertions.
pipeline and backpressure?
Inherited from stream semantics - pipeline does not remove need for sensible highWaterMark.
Related
- Streams Basics - pipe introduction
- Backpressure - flow control
- Streaming HTTP Responses - proxies and headers
- Streams Best Practices - always pipeline rule
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.