Skip to the solution
MediumLow-Level Design7 min read

Why is stream.pipeline() preferred over .pipe() for chaining streams?

159 views

The problem

Question presented to candidate: "You chained three streams together with .pipe(). One of them errors midway through. What happens to the other two, and is that a problem?"

What a strong answer should cover:

  • .pipe() only forwards data, not errors, between streams. If the source errors, the destination is left exactly as it was β€” nothing tells it to stop or clean up.
  • The practical consequence: file descriptors, sockets, or other resources held by the un-destroyed downstream streams can leak, because nothing closed them. This is the actual, concrete cost of the "silent" half of the problem, not an abstract inconvenience.
  • stream.pipeline() (available as a callback API and, more idiomatically today, as a promise via stream/promises) does two things .pipe() does not: it forwards an error from any stream in the chain to every other stream, destroying all of them, and it gives you one single place (the callback or the awaited promise) to find out the chain finished or failed β€” rather than needing an error listener on every individual stream.
  • pipeline() also correctly propagates a chain's completion β€” the promise resolves only once the entire chain (not just the first stream) has finished, which matters for chains ending in an async destination.
  • The old workaround before pipeline() existed β€” attaching an "error" listener to every stream in the chain and manually calling .destroy() on the others β€” is real code many older codebases still have, and it is exactly the boilerplate pipeline() was built to eliminate.
  • .pipe() is not simply "wrong" or removed β€” it remains fine for a quick, throwaway script where a leaked resource on error genuinely does not matter. The recommendation is specific to production chains, especially ones involving file or network I/O.

Clarifying questions expected:

  • "Does any stream in this chain hold an external resource β€” a file descriptor, a socket β€” that must be closed on failure?" β€” that is exactly the risk .pipe() does not cover.
  • "Is this chain in a one-off script or in a long-running service?" β€” decides how much the leak risk actually matters here.

Code / implementation expected: Yes β€” showing the same failing chain under both .pipe() and pipeline(), with the destination's actual destroyed state, is the clearest way to make the difference concrete.

  • streams
  • pipeline
  • pipe
  • error-handling

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

7 min read Β· 8 sections

Read the code

JavaScriptIdentical source-error scenario through .pipe() (leaves destination undestroyed) vs pipeline() (cleans up + reports)
import { Readable, Transform, Writable } from "node:stream";
import { pipeline } from "node:stream/promises";

function makeErroringReadable() {
  let n = 0;
  return new Readable({
    read() {
      n++;
      if (n === 3) { this.destroy(new Error("boom from source")); return; }
      this.push(`chunk${n}\n`);
    },
  });
}

// .pipe(): the destination is left exactly as it was.
const src = makeErroringReadable();
const dest = new Writable({ write(c, e, cb) { cb(); } });
src.pipe(new Transform({ transform(c, e, cb) { cb(null, c); } })).pipe(dest);
setTimeout(() => {
  console.log("pipe(): dest.destroyed after 300ms =", dest.destroyed); // false
}, 300);

// pipeline(): destination is destroyed AND the error is reported in one place.
try {
  const src2 = makeErroringReadable();
  const dest2 = new Writable({ write(c, e, cb) { cb(); } });
  dest2.on("close", () => console.log("pipeline(): destination closed automatically"));
  await pipeline(src2, new Transform({ transform(c, e, cb) { cb(null, c); } }), dest2);
} catch (err) {
  console.log("pipeline(): rejected with:", err.message); // "boom from source"
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 108 of 152 in Node.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Node.js questions
Up nextMedium

How do you build a GraphQL API in Node.js, and what is the N+1 problem?

Continue