stream.finished()

Detect when a stream has ended, errored, or closed prematurely.

Signature
stream.finished(stream[, options], callback)
  • Added in Node.js v10.0.0

Parameters

ParameterTypeDescription
$streamStream | ReadableStream | WritableStreamA readable and/or writable stream/webstream.
$optionsoptionalObjectOptions controlling which conditions count as finished.
$options.erroroptionalboolean= trueIf set to false, then a call to emit('error', err) is not treated as finished.
$options.readableoptionalboolean= trueWhen set to false, the callback will be called when the stream ends even though the stream might still be readable.
$options.writableoptionalboolean= trueWhen set to false, the callback will be called when the stream ends even though the stream might still be writable.
$options.signaloptionalAbortSignalAllows aborting the wait for the stream to finish. The underlying stream will not be aborted; the callback is called with an AbortError and all listeners added by this function are removed.
$callbackFunctionA callback function that takes an optional error argument.

Return value

FunctionA cleanup function which removes all registered listeners.

Errors

Does not throw; failures are reported to the callback, including ERR_STREAM_PREMATURE_CLOSE when a stream closes before ending and AbortError when the signal option is aborted.

Changelog

  • PHP v19.5.0Added support for ReadableStream and WritableStream web streams.
  • PHP v15.11.0The signal option was added.
  • PHP v14.0.0finished(stream, cb) now waits for the 'close' event before invoking the callback on streams expected to emit it, so the callback fires later than before.
  • PHP v14.0.0Emitting 'close' before 'end' on a Readable now causes an ERR_STREAM_PREMATURE_CLOSE error.
  • PHP v14.0.0The callback is now invoked on streams that had already finished before finished() was called.

finished() calls you back exactly once when a stream is no longer readable or writable — whether it ended cleanly, emitted an error, or was destroyed before finishing. It exists because no single event covers all three: 'end', 'finish', 'error', and 'close' each cover part of the story, and the combination differs between stream types.

When to reach for it

Cleanup around a stream you did not create the pipeline for. The canonical case is an aborted HTTP request: the client disconnects, the request stream is destroyed, and neither 'end' nor 'finish' ever fires. finished() reports it as ERR_STREAM_PREMATURE_CLOSE, which is your cue to roll back a transaction or delete a partial upload.

The promise form in node:stream/promises is usually nicer: await finished(writeStream) is the clean way to wait for a file to be fully flushed and closed.

What trips people up

It observes; it does not consume. Call finished() on an unconsumed readable and the callback never fires, because the stream never ends — you still need resume(), a 'data' handler, or a pipe.

It also leaves its listeners attached after the callback runs. On a long-lived stream that you call it on repeatedly, that is a leak; call the returned cleanup function, or pass cleanup: true in the promise form.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Delete a partial upload when the client disconnects mid-request
  • Wait for a file write stream to be fully flushed and closed before reading it
  • Release a database connection once a response stream is done, however it ended
  • Distinguish a clean end from a premature close in request logging

Frequently asked questions

How do I know when a stream is finished in Node.js?

Call stream.finished(stream, callback), which fires once whether the stream ended cleanly, errored, or was destroyed prematurely. The promise form in node:stream/promises lets you await it instead.

What is ERR_STREAM_PREMATURE_CLOSE?

It is the error finished() reports when a stream was destroyed or closed before it ended or finished — most often an HTTP request whose client disconnected mid-body. Treat it as a signal to roll back partial work.

Why does my finished() callback never fire?

Because finished() only observes the stream, it does not consume it, so a paused readable that nothing reads never ends. Call resume(), attach a 'data' handler, or pipe it somewhere.

More examples

Cleaning up after an aborted upload

Without finished(), an aborted upload leaves the .part file on disk forever: the request stream is destroyed, so 'end' never fires and the success path never runs. Here the error branch sees ERR_STREAM_PREMATURE_CLOSE and removes the partial file. Note that req.pipe(out) is still required — finished() only watches.

Example · javascript
const http = require('node:http');
const fs = require('node:fs');
const { finished } = require('node:stream');

http.createServer((req, res) => {
  const tmp = `upload-${Date.now()}.part`;
  const out = fs.createWriteStream(tmp);

  // Fires on clean end AND on client disconnect, which 'end' alone does not.
  finished(req, (err) => {
    if (err) {
      // ERR_STREAM_PREMATURE_CLOSE when the client went away mid-body.
      out.destroy();
      fs.rm(tmp, { force: true }, () => {});
      console.log('upload aborted:', err.code);
      return;
    }
    fs.renameSync(tmp, tmp.replace('.part', '.bin'));
    res.end('ok');
  });

  req.pipe(out);
}).listen(3000);

The promise form with automatic cleanup

The promise form is the idiomatic way to wait for a write stream to be fully flushed and closed before reading the file — end() alone does not guarantee that. cleanup: true, added in v19.1.0, detaches the listeners so repeated awaits on a long-lived stream do not accumulate them; the callback form requires calling the returned cleanup function instead.

Example · javascript
import { finished } from 'node:stream/promises';
import fs from 'node:fs';

const out = fs.createWriteStream('report.txt');
out.write('first\n');
out.end('second\n');

// cleanup: true removes the listeners before the promise settles,
// which matters on long-lived streams awaited repeatedly.
await finished(out, { cleanup: true });

console.log(fs.readFileSync('report.txt', 'utf8'));
fs.rmSync('report.txt');

// Waiting only for the readable side of a duplex:
//   await finished(socket, { writable: false });

Discussion

  • Be the first to comment on this lesson.