Skip to content

spec: readable-stream v2;v3;v4 to node:stream v24 #471

Description

@AugustinMauroy

Description

We should document and codemod the migration from readable-stream v2/v3/v4 to node:stream v24.

  • The migration should be divided by readable-stream v2, v3, and v4.
  • The migration should separate import replacement from runtime behavior differences.
  • The migration should call out observed output gaps so version-specific behavior is explicit.
  • The migration should keep the stream wiring and example structure aligned across all baselines.
  • The migration should include compensating patterns for code that depends on legacy timing behavior.

Important points

  • readable-stream v2 and v3 share the same legacy timing behavior in the observed gaps.
  • readable-stream v4 matches node:stream v24 for the observed gaps.
  • Most cases are a straight source-import migration, but a few cases need behavior notes because the output differs at runtime.
  • The codemod should prefer built-in node:stream imports for Node.js-only code.
  • When runtime timing changes matter, the issue should show a safer migration pattern that avoids the late error.

Examples

readable-stream v2 -> node:stream v24

- import { Readable } from "readable-stream";
+ import { Readable } from "node:stream";

Observed runtime gap:

--- readable-stream v2
+++ node:stream v24
@@
 a
+ERR:boom

Example of potential migration:

 const { Readable } = require("readable-stream");

 const readable = Readable.from(["a"]);

-readable.destroy(new Error("boom"));
+readable.once("error", (error) => {
+  console.error(error.message);
+});
+
+setImmediate(() => {
+  readable.destroy(new Error("boom"));
+});

Example of safer migration for post-data error handling:

 const { Readable } = require("readable-stream");

 const readable = Readable.from(["a"]);

-readable.on("data", (chunk) => {
-  console.log(chunk.toString());
-});
-
-readable.on("error", (error) => {
-  console.error(error.message);
-});
+readable.once("error", (error) => {
+  console.error(error.message);
+});
+
+readable.on("data", (chunk) => {
+  console.log(chunk.toString());
+});

readable-stream v3 -> node:stream v24

- import { Readable, Transform, Writable, pipeline } from "readable-stream";
+ import { Readable, Transform, Writable, pipeline } from "node:stream";

Observed runtime gap:

--- readable-stream v3
+++ node:stream v24
@@
 a
+ERR:boom

Second observed gap:

--- readable-stream v3
+++ node:stream v24
@@
+ERR:readable-err

Example of potential migration for late destroy timing:

 const { Readable } = require("readable-stream");

 const readable = Readable.from(["a"]);

-readable.destroy(new Error("boom"));
+readable.once("error", (error) => {
+  process.stderr.write(`${error.message}\n`);
+});
+
+queueMicrotask(() => {
+  readable.destroy(new Error("boom"));
+});

Example of potential migration for post-data error handling:

 const { Readable } = require("readable-stream");

 const readable = Readable.from(["a"]);

-readable.on("data", (chunk) => {
-  console.log(chunk.toString());
-});
+readable.once("error", (error) => {
+  process.stderr.write(`${error.message}\n`);
+});
+
+readable.on("data", (chunk) => {
+  console.log(chunk.toString());
+});

readable-stream v4 -> node:stream v24

- import * as stream from "readable-stream";
+ import * as stream from "node:stream";

Observed runtime behavior:

--- readable-stream v4
+++ node:stream v24
@@
 (no changes)

Example of the same migration pattern with no compensation needed:

 const stream = require("readable-stream");

 const readable = stream.Readable.from(["a"]);

-readable.on("data", console.log);
+readable.on("data", console.log);

Caveats

  • These are runtime timing differences, not API surface differences.
  • The migration is usually a simple import swap, but the version split matters when code depends on late destroy() behavior or errors emitted after the first data event.
  • v4 aligns with Node core on the observed cases, while v2 and v3 preserve the older behavior.
  • If the code relies on the legacy timing, add explicit error listeners before the operation that may fail.
  • If the code only needs the stream output and not the timing artifact, prefer the Node core behavior and remove the dependency on the old ordering.

Refs

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels
    No fields configured for Feature.

    Projects

    Status
    📋 Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions