@ljharb/through review
@ljharb/through 2.3.14 is a maintained fork of the original `through` helper for old-style Node streams. It creates an object that is readable and writable, then binds that stream as `this` inside synchronous write and end callbacks. `this.queue(chunk)` sends or buffers output; `this.queue(null)` ends the readable side. The fork adds bundled TypeScript declarations and dependency upkeep while retaining the 2012-era contract. It fits legacy pipe chains, not new asynchronous transforms.
Our @ljharb/through 2.3.14 install used 2 MB and passed npm audit, but its browser bundle failed and its synchronous stream contract still dates to the original helper. Install it for compatibility with old `through` code; use built-in `Transform` for a new pipeline.
We installed it
| Install | ✓ · 1.7s | 17 packages on disk · 2 MB |
| Import | ✓ | ESM import works · require() works · CommonJS package |
| Browser | n/a | could not be bundled for the browser (Node-only code, most likely) |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does @ljharb/through install cleanly?
Yes. In a fresh container with an empty cache, npm install @ljharb/through finished in 2 seconds, leaving 17 packages and 2 MB on disk. npm audit reported no known vulnerabilities.
Can @ljharb/through run in a browser?
Not directly: esbuild could not bundle it for the browser in our run, which normally means it depends on Node built-ins. Use it on the server, or find a browser-targeted alternative.
Does @ljharb/through work with both ESM and CommonJS?
Yes. Both import '@ljharb/through' and require('@ljharb/through') worked in Node 22 in our run. The package is published as CommonJS.
Does @ljharb/through include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
@ljharb/through or through2: which should you use?
through2: Use it for callback transforms backed by Node's newer Transform stream contract. Our @ljharb/through 2.3.14 install used 2 MB and passed npm audit, but its browser bundle failed and its synchronous stream contract still dates to the original helper.
When should you not use @ljharb/through?
You are starting new Node code: built-in Transform and PassThrough avoid this dependency and follow current stream lifecycle rules
Use it if
- An existing module expects the original `through(write, end, options)` callback and `this.queue()` contract
- You need a small synchronous filter or mapper in a CommonJS Node stream chain
- You are migrating from unscoped `through` and want the compatible fork with bundled declarations
- Supporting Node versions far older than current `node:stream` helpers is a real project requirement
- You are starting new Node code: built-in `Transform` and `PassThrough` avoid this dependency and follow current stream lifecycle rules
- The transform awaits I/O: its write function has no callback or Promise protocol, so delayed output breaks the intended flow control
- You need `objectMode`, `highWaterMark`, `_flush`, or `_final` configuration: the README documents only `autoDestroy`
- You publish ESM-only packages: 2.3.14 is CommonJS with no exports map and its declarations use the CommonJS export shape
- You need browser code: our esbuild browser bundle failed, consistent with a helper built on Node's stream implementation
Setup reality
We installed @ljharb/through 2.3.14 in our sandbox in 1.7 seconds. The result was 17 packages and 2 MB on disk, with 0 known vulnerabilities from npm audit. The package is 84 KB unpacked, has 1 direct dependency, 0 peer dependencies, an MIT license, and an engine floor of Node 0.4. There was no native build, credential prompt, or configuration file.
The published entry point is CommonJS and has no exports map. require() and ESM import both worked under Node 22, and TypeScript declarations are bundled. esbuild could not produce a browser bundle in our test, which matches the package's dependence on Node stream behavior. Treat it as server-side code even if a bundler can polyfill parts of the stream stack.
Callbacks must be ordinary functions because the package binds the stream as this. The write path is synchronous and supplies no completion callback. this.queue() buffers chunks while paused, whereas this.emit('data', value) bypasses that buffer. Null is the end signal, so a custom end callback must call this.queue(null) or emit end; forgetting it leaves readers waiting.
Its pressure signal is also old-fashioned: write() returns the inverse of this.paused, not a high-water-mark calculation. A long pause can accumulate an unbounded internal buffer. Thrown callback errors escape synchronously, and source errors do not gain modern pipeline teardown just because this helper is present. Put the chain inside node:stream.pipeline() when coordinated cleanup matters.
Patterns
Create an unchanged pipe stage pass-through
const through = require('@ljharb/through');
const stage = through();
source.pipe(stage).pipe(destination);With no callbacks, each input chunk is queued unchanged and the default end callback queues null.
Rewrite chunks synchronously map-chunks
const upper = through(function (chunk) {
this.queue(Buffer.from(chunk.toString().toUpperCase()));
});Use a normal function. An arrow function cannot receive the stream through the bound `this` value.
Drop chunks that fail a check filter-chunks
const nonempty = through(function (chunk) {
if (chunk.length > 0) this.queue(chunk);
});Returning without `queue()` discards that chunk; the write call still finishes synchronously.
Produce several outputs from one input emit-many
const duplicate = through(function (chunk) {
this.queue(chunk);
this.queue(chunk);
});Every queued value becomes a data chunk. Null is reserved for ending the readable side.
Append output before the stream ends flush-final-value
const footer = through(
function (chunk) { this.queue(chunk); },
function () {
this.queue('done\n');
this.queue(null);
}
);Providing an end callback replaces the default one. Queue null yourself or downstream readers will keep waiting.
Pause and later drain queued chunks pause-output
const gate = through();
gate.pause();
source.pipe(gate);
setTimeout(() => gate.resume(), 100);The queue has no configurable size ceiling. A producer can fill memory if the pause lasts while writes continue.
Observe the legacy pressure signal check-write-result
if (!stage.write(chunk)) {
stage.once('drain', sendNext);
} else {
sendNext();
}`write()` returns false when this stream is paused. It does not calculate pressure from a writable highWaterMark.
Write one last value and finish end-with-chunk
const stage = through();
stage.on('data', console.log);
stage.end('last');`end(value)` writes the supplied value before it runs the end callback, and later end calls are ignored.
Keep close separate from both end states disable-auto-close
const stage = through(write, end, { autoDestroy: false });`autoDestroy` defaults to true and emits close after the readable and writable sides have ended. Change it only for a legacy consumer that expects another sequence.
Give the chain coordinated teardown pipeline-errors
const { pipeline } = require('node:stream');
const stage = through(function (chunk) { this.queue(chunk); });
pipeline(source, stage, destination, error => {
if (error) console.error(error);
});`pipeline()` centralizes errors and destroys the chain. The through helper alone does not provide that modern teardown behavior.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| through2 | npm | Use it for callback transforms backed by Node's newer Transform stream contract. |
| stream-transform | npm | Use it for record transforms that need asynchronous completion or parallel work. |
| minipass | npm | Use it when you want a compact modern stream implementation with a different API. |
More utils guides
lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

