stream.finished()
Detect when a stream has ended, errored, or closed prematurely.
stream.finished(stream[, options], callback)- Added in Node.js v10.0.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$stream | Stream | ReadableStream | WritableStream | A readable and/or writable stream/webstream. |
$optionsoptional | Object | Options controlling which conditions count as finished. |
$options.erroroptional | boolean= true | If set to false, then a call to emit('error', err) is not treated as finished. |
$options.readableoptional | boolean= true | When set to false, the callback will be called when the stream ends even though the stream might still be readable. |
$options.writableoptional | boolean= true | When set to false, the callback will be called when the stream ends even though the stream might still be writable. |
$options.signaloptional | AbortSignal | Allows 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. |
$callback | Function | A 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
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.
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.
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