<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: r9v</title>
    <description>The latest articles on DEV Community by r9v (@r9v).</description>
    <link>https://dev.to/r9v</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3954483%2Fd0392460-e31e-43d1-84ed-d22513ca0a99.jpeg</url>
      <title>DEV Community: r9v</title>
      <link>https://dev.to/r9v</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/r9v"/>
    <language>en</language>
    <item>
      <title>Node streams aren't hard anymore</title>
      <dc:creator>r9v</dc:creator>
      <pubDate>Wed, 27 May 2026 14:34:24 +0000</pubDate>
      <link>https://dev.to/r9v/node-streams-arent-hard-anymore-5794</link>
      <guid>https://dev.to/r9v/node-streams-arent-hard-anymore-5794</guid>
      <description>&lt;p&gt;Node streams have a reputation. James Halliday wrote a "stream-handbook" repo over a decade ago that became canonical, and the existence of a &lt;em&gt;handbook&lt;/em&gt; told you everything. For years, asking a Node developer about backpressure was a way to find out which job they were about to quit.&lt;/p&gt;

&lt;p&gt;The reputation was earned, but it's mostly obsolete now. The API got fixed in stages between 2018 and 2021, and nobody made a clean announcement of it, so the cultural memory stayed at "streams are scary" while the actual code became, for most uses, boring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why they were hard
&lt;/h2&gt;

&lt;p&gt;The short version: three eras of API coexisting in one type, errors that didn't propagate through &lt;code&gt;.pipe()&lt;/code&gt;, backpressure as an advisory boolean that ninety percent of code ignored, and four different events all roughly meaning "done" ('end', 'finish', 'close', plus a &lt;code&gt;null&lt;/code&gt; from &lt;code&gt;read()&lt;/code&gt;). The longer version is the stream-handbook itself.&lt;/p&gt;

&lt;p&gt;Pre-2018, writing correct stream code meant remembering, for every pipeline, that you had to attach &lt;code&gt;'error'&lt;/code&gt; to every stage because &lt;code&gt;.pipe()&lt;/code&gt; didn't propagate errors and a downstream failure would orphan the upstream stages, hanging the process or leaking file descriptors. You had to check the return value of &lt;code&gt;.write()&lt;/code&gt; and listen for &lt;code&gt;'drain'&lt;/code&gt;, because otherwise your writable buffer grew until the process OOMed. You had to know which mode (paused or flowing) your readable was in, because attaching a &lt;code&gt;'data'&lt;/code&gt; listener flipped it, and once flowing you could lose chunks if you attached late. And you had to distinguish &lt;code&gt;'end'&lt;/code&gt; (no more reads) from &lt;code&gt;'finish'&lt;/code&gt; (all writes flushed) from &lt;code&gt;'close'&lt;/code&gt; (resource released), because they fired at different times and meant different things.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed
&lt;/h2&gt;

&lt;p&gt;The fix came in pieces.&lt;/p&gt;

&lt;p&gt;In 2018, Node 10 shipped &lt;code&gt;stream.pipeline()&lt;/code&gt;. It propagates errors, destroys all stages on failure, and cleans up resources, the single biggest improvement to streams in Node's history, and it arrived without fanfare. The same release made Readables async iterables, so &lt;code&gt;for await (const chunk of stream)&lt;/code&gt; started working, hiding the entire mode/event apparatus behind one line.&lt;/p&gt;

&lt;p&gt;In 2020, Node 15 added &lt;code&gt;stream/promises&lt;/code&gt;, giving you &lt;code&gt;await pipeline(...)&lt;/code&gt; and &lt;code&gt;await finished(stream)&lt;/code&gt;. Promise-native, no event handlers.&lt;/p&gt;

&lt;p&gt;In 2021, Node 16.5 added Web Streams as experimental (they became stable in Node 21, late 2023), a different model entirely, promise-based, never inheriting from EventEmitter, for code that wants out of the legacy.&lt;/p&gt;

&lt;p&gt;Most stream tutorials today still walk you through these chronologically: events first, then &lt;code&gt;.pipe()&lt;/code&gt;, then &lt;code&gt;pipeline()&lt;/code&gt;, then &lt;code&gt;for await&lt;/code&gt;, then Web Streams. You finish with five overlapping mental models and the feeling that streams have too many APIs.&lt;/p&gt;

&lt;p&gt;There's a better framing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The map
&lt;/h2&gt;

&lt;p&gt;Every Node stream API fits into one 2×2:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Readable&lt;/th&gt;
&lt;th&gt;Writable&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Consume&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;how you take data out&lt;/td&gt;
&lt;td&gt;how you put data in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Implement&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;how you produce data inside&lt;/td&gt;
&lt;td&gt;how you receive data inside&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Four cells. Every API you've ever seen on a stream lives in exactly one of them. The complexity of Node streams comes from each cell having multiple layers of API stacked on top, accreted over different versions, and the clarity comes from seeing the cells as separate problems.&lt;/p&gt;

&lt;p&gt;Let's fill them in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Consume Readable, "how do I take data out of this thing"
&lt;/h3&gt;

&lt;p&gt;Three layers, low to high:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Layer 1: paused mode, manual&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;readable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;readable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;readable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="cm"&gt;/* try .read() again */&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Layer 2: flowing mode, push&lt;/span&gt;
&lt;span class="nx"&gt;readable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;readable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;end&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;done&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="c1"&gt;// Layer 3: async iteration (Node 10+)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;readable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Layer 3 hides everything below it. Backpressure is automatic because &lt;code&gt;await&lt;/code&gt; blocks the loop, errors throw out of the &lt;code&gt;for await&lt;/code&gt;, and stream mode is irrelevant because you never see it. For consumption, this is the only API you need now.&lt;/p&gt;

&lt;h3&gt;
  
  
  Consume Writable, "how do I put data into this thing"
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Layer 1: manual, with backpressure&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;writable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;once&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;writable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;drain&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;writable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;writable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;finish&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;done&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="c1"&gt;// Layer 2: pipeline (Node 10+, promise variant Node 15+)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;writable&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no &lt;code&gt;for await&lt;/code&gt; equivalent for writables, you can't async-iterate a sink. The modern answer is to never call &lt;code&gt;.write()&lt;/code&gt; yourself, let &lt;code&gt;pipeline()&lt;/code&gt; do it. The boolean-return-value-plus-drain dance is still the underlying protocol, but your application code shouldn't be running it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implement Readable, "I want to produce data"
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Layer 1: raw&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyReadable&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Readable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;_read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// returns false when buffer is full&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// signal end&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Layer 2: from an async iterable (Node 12+)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Readable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(...))&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Readable.from&lt;/code&gt; covers most cases. You write a generator, sync or async, and Node wraps it into a Readable that handles &lt;code&gt;_read&lt;/code&gt;, &lt;code&gt;push&lt;/code&gt;, backpressure, and end-of-stream for you. For most custom Readables, this is what you reach for.&lt;/p&gt;

&lt;p&gt;The raw &lt;code&gt;_read&lt;/code&gt;/&lt;code&gt;push&lt;/code&gt; API is still current, it's what you fall back to when you need fine control, things like multiple data sources or integration with a non-iterable producer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implement Writable, "I want to receive data"
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Writable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the cell that didn't get sugar. There's no &lt;code&gt;Writable.from(asyncFn)&lt;/code&gt; that wraps a consumer function into a properly backpressured Writable. You either implement the &lt;code&gt;write(chunk, enc, cb)&lt;/code&gt; callback form or you extend the &lt;code&gt;Writable&lt;/code&gt; class and override &lt;code&gt;_write&lt;/code&gt;. Calling &lt;code&gt;callback()&lt;/code&gt; participates in backpressure, Node won't call &lt;code&gt;write&lt;/code&gt; again until you signal completion.&lt;/p&gt;

&lt;p&gt;Most application code doesn't need to implement Writables, the built-in ones (&lt;code&gt;fs.createWriteStream&lt;/code&gt;, the HTTP response, network sockets) cover the common sinks. When you do need one, the raw API is verbose but fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the map shows
&lt;/h2&gt;

&lt;p&gt;The two &lt;strong&gt;consume&lt;/strong&gt; cells got modern wrappers, &lt;code&gt;for await&lt;/code&gt; and &lt;code&gt;pipeline()&lt;/code&gt;. Most application code lives in those two cells, which is why most stream code is now easy.&lt;/p&gt;

&lt;p&gt;The two &lt;strong&gt;implement&lt;/strong&gt; cells got partial sugar (&lt;code&gt;Readable.from&lt;/code&gt;) or none (&lt;code&gt;Writable&lt;/code&gt;). This is where the historical complexity still lives, and it's narrower than the reputation suggests, you have to be writing your own stream type to hit it.&lt;/p&gt;

&lt;p&gt;Web Streams are the same 2×2 with different method names. Consumption goes through &lt;code&gt;getReader().read()&lt;/code&gt;, production through a controller's &lt;code&gt;enqueue()&lt;/code&gt; from inside the source. The Writable side has its own surface, a &lt;code&gt;WritableStream&lt;/code&gt; constructed with a &lt;code&gt;write()&lt;/code&gt; method on its underlying sink. The shape is identical, what's missing is the EventEmitter row beneath each cell. Web Streams never accumulated that layer, so there's one API per cell instead of two or three.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means in practice
&lt;/h2&gt;

&lt;p&gt;If you're consuming streams in application code, you're in the top row of the map, and you should be using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Reading&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;readable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="cm"&gt;/* ... */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Writing / piping&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything below that, the raw event API and &lt;code&gt;.pipe()&lt;/code&gt; without &lt;code&gt;pipeline&lt;/code&gt;, is the legacy stack. It still works, and you'll encounter it in older code, in Express middleware, in libraries that haven't updated. You should be able to read it, but you shouldn't be writing it.&lt;/p&gt;

&lt;p&gt;If you're implementing custom streams, you're in the bottom row, where the API is still raw. &lt;code&gt;Readable.from&lt;/code&gt; covers most Readable cases via generators. For Writables, you write &lt;code&gt;new Writable({ write(c, e, cb) {} })&lt;/code&gt; and it's fine, the verbosity is the cost.&lt;/p&gt;

&lt;p&gt;If you're starting a new project that might run outside Node, on Workers or in browsers, skip the Node stream API entirely and use Web Streams. Same model, smaller surface, portable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reputation, revisited
&lt;/h2&gt;

&lt;p&gt;Node streams were hard because the API accumulated rather than redesigned. Each fix added a new layer without removing the old ones, and for a long time, that meant you had to learn all the layers to write correct code.&lt;/p&gt;

&lt;p&gt;That's no longer true. The top of the stack, &lt;code&gt;for await&lt;/code&gt; and &lt;code&gt;pipeline()&lt;/code&gt;, handles the common cases cleanly. The bottom is still there for performance-critical paths and custom stream implementations, but it's no longer on the daily path.&lt;/p&gt;

&lt;p&gt;The handbook isn't wrong, it's just no longer the prerequisite it was.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>webdev</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
