mrkeyoor.com_
Sat 08 Aug 22:01 UTC
npmUtilsupdated 08 Aug 2026

@ljharb/through

A maintained fork of Dominic Tarr's tiny `through` module for constructing an old-style Node readable and writable stream from synchronous write and end callbacks. Call `this.queue(chunk)` inside the write callback to emit or buffer output, and `this.queue(null)` to end the readable side. The fork keeps the original API while adding TypeScript declarations and dependency upkeep. It is compatibility infrastructure for packages built around classic streams, not a modern replacement for Node's Transform class.

Verdict

A careful maintenance fork for software already coupled to `through`, with types as its clearest improvement. For new code, Node's built-in Transform or through2 gives you a stream contract that handles modern backpressure and asynchronous work more honestly.

API stability5/5The callback, queue, pause, resume, and autoDestroy contract dates to the original through 2.x line and remains intact in 2.3.14. The fork's releases have focused on types, metadata, tests, and dependency updates rather than behavioral reinvention. That makes compatibility excellent, but the stability comes from freezing an old stream model with very few extension points.
Docs3/5The README clearly shows the core write and end callbacks, explains queue-based buffering, contrasts direct data emission, and documents autoDestroy. Bundled declarations accurately list the custom methods and return types. It does not explain asynchronous callback hazards, error propagation, null termination, TypeScript import syntax, or how its base stream.Stream implementation differs from modern Transform streams, leaving important production behavior to source inspection.
Maintenance3/5The unarchived fork released 2.3.14 and pushed its repository in February 2025, with recent work covering declarations, tests, dependency updates, and package hygiene. GitHub currently reports no open issues or pull requests. There has been no push since that release, but the implementation is tiny and intentionally stable, so low activity is less alarming here than it would be in a network or build tool.
Ecosystem3/5The scoped fork recorded 3,627,809 downloads in the measured npm week, largely reflecting its role in established dependency trees and migration from the original through package. It accepts ordinary Node pipe chains and carries its own TypeScript declaration. Still, it has only 4 GitHub stars, exposes an old CommonJS interface, and competes with Node's built-in streams plus better-known through2, so direct adoption remains niche.

Use it if

  • You maintain a dependency that already expects the original through 2.3 callback and queue API
  • You need a small synchronous pass-through, filter, or one-input-to-many-output stream in CommonJS
  • You need the old through API with bundled TypeScript declarations and a maintained npm fork
  • You must preserve support for very old Node versions where modern stream helpers are unavailable
Skip it if

Setup reality

Installation is just `npm install @ljharb/through`; there are no peer dependencies, credentials, native builds, or configuration files. The friction is conceptual. This package deliberately preserves a stream design from 2012. Import it with require(), then pass ordinary functions, not arrow functions, because the package supplies the stream as `this`. Output goes through `this.queue(value)` or its `push` alias. Passing null is not data: it marks the readable side ended, and later queue calls are ignored. The write callback is synchronous and receives no `done` callback, so it is not a safe place for awaited I/O or delayed output. queue() buffers while paused and drains on resume; directly emitting `data` bypasses that buffering. write() reports `!paused`, but the implementation does not expose modern highWaterMark-based pressure controls. The default end callback queues null, while a custom end callback must do that itself or explicitly emit end. By default autoDestroy emits close after both sides end; set it false only if older consumers require a different close lifecycle. Errors thrown by your callback propagate synchronously, and errors from piped sources are not automatically forwarded, so add error listeners or use node:stream pipeline around the chain. TypeScript declarations are included, but they use `export =`, matching `import through = require('@ljharb/through')` or a default import only when your compiler interop settings allow it. The package depends on call-bind and supports Node 0.4+, a compatibility range that explains the old API rather than recommending it for current code.

Patterns

Create a transparent pass-through streampass-through

const through = require('@ljharb/through')

const passthrough = through()
source.pipe(passthrough).pipe(destination)

With no callbacks, each chunk is queued unchanged and the readable side ends when the writable side ends.

Transform each chunk synchronouslymap-chunks

const upper = through(function write(chunk) {
  this.queue(Buffer.from(chunk.toString().toUpperCase()))
})

source.pipe(upper).pipe(process.stdout)

Use a normal function so `this` is the through stream. Arrow functions cannot receive the bound stream context.

Drop unwanted chunksfilter-chunks

const nonEmpty = through(function write(chunk) {
  if (chunk.length > 0) this.queue(chunk)
})

Not calling queue drops the input chunk. The write callback still completes synchronously.

Emit multiple chunks for one inputexpand-chunks

const lines = through(function write(chunk) {
  for (const line of chunk.toString().split('\n')) {
    this.queue(line + '\n')
  }
})

Chunk boundaries are arbitrary in byte streams. This example splits each chunk independently and is not a correct parser for lines spanning chunks.

Emit a final chunk before endingflush-on-end

const footer = through(
  function write(chunk) { this.queue(chunk) },
  function end() {
    this.queue('\n-- done --\n')
    this.queue(null)
  },
)

A custom end callback replaces the default, so it must queue null to finish the readable side.

Pause output and drain the internal buffer laterpause-and-resume

const gate = through()
gate.pause()
source.pipe(gate)

setTimeout(() => {
  gate.resume()
  gate.pipe(destination)
}, 100)

queue() buffers while paused. The buffer has no configurable size limit, so a long pause can retain unbounded data.

Honor the boolean returned by writeinspect-backpressure

if (!transform.write(chunk)) {
  transform.once('drain', writeNext)
} else {
  writeNext()
}

write() returns false only when this stream is paused. It does not use a modern writable highWaterMark. Prefer pipe() or pipeline() when possible.

Write a last chunk and endend-with-data

const stream = through()
stream.on('data', (chunk) => console.log(chunk))
stream.end('last value')

end(value) writes the value before running the end callback. Repeated end calls are ignored.

End output earlystop-readable-side

const firstMatch = through(function write(chunk) {
  if (chunk.toString().includes('needle')) {
    this.queue(chunk)
    this.queue(null)
  }
})

After queue(null), later queue calls are ignored, but the writable side can still receive input until it is ended or destroyed.

Keep close from firing automaticallydisable-auto-destroy

const stream = through(
  function write(chunk) { this.queue(chunk) },
  null,
  { autoDestroy: false },
)

The default is true. Disable it only for consumers that depend on the legacy end and close sequence, then destroy the stream yourself.

Emit data without pause bufferingbypass-buffering

const immediate = through(function write(chunk) {
  this.emit('data', chunk)
}, function end() {
  this.emit('end')
})

Direct data emission bypasses the package's queue and pause buffer. The README documents this mode, but it also bypasses the main reason to use the helper.

Wrap a through stream in pipelinepipeline-errors

const { pipeline } = require('node:stream')
const through = require('@ljharb/through')

const transform = through(function write(chunk) {
  this.queue(chunk)
})

pipeline(source, transform, destination, (error) => {
  if (error) console.error('pipeline failed', error)
})

The helper does not automatically forward every source error. pipeline centralizes teardown and error reporting for the chain.

Alternatives

PackageRegistryPick it when
through2npmYou need the familiar callback transform style with Streams2 Transform behavior and object-mode constructors
stream-transformnpmYou need asynchronous record transformation with parallelism and a completion callback
streamxnpmYou want a modern userland stream implementation with explicit readable and writable classes
throughnpmAn existing dependency requires the original unscoped package and changing identity would add no value