mongodb/node-mongodb-native · error · MongoChangeStreamError
ChangeStream is closed
Error message
ChangeStream is closed
What it means
Thrown by ChangeStream.stream() when you attempt to obtain a Readable stream from a change stream that has already been closed (this.closed === true, set by close()). It is a MongoChangeStreamError with the literal message 'ChangeStream is closed'. The guard exists because the underlying cursor has already been torn down and piping it would produce undefined behavior.
Source
Thrown at src/change_stream.ts:870
const cursor = this.cursor;
try {
await cursor.close();
} finally {
this._endStream();
}
}
/**
* Return a modified Readable stream including a possible transform method.
*
* NOTE: When using a Stream to process change stream events, the stream will
* NOT automatically resume in the case a resumable error is encountered.
*
* @throws MongoChangeStreamError if the underlying cursor or the change stream is closed
*/
stream(): Readable & AsyncIterable<TChange> {
if (this.closed) {
throw new MongoChangeStreamError(CHANGESTREAM_CLOSED_ERROR);
}
return this.cursor.stream();
}
/** @internal */
private _setIsEmitter(): void {
if (this.mode === 'iterator') {
// TODO(NODE-3485): Replace with MongoChangeStreamModeError
throw new MongoAPIError(
'ChangeStream cannot be used as an EventEmitter after being used as an iterator'
);
}
this.mode = 'emitter';
}
/** @internal */
private _setIsIterator(): void {View on GitHub (pinned to 3366c21a63)
Solutions
- Create a fresh ChangeStream via collection.watch()/db.watch() before calling .stream() instead of reusing the closed one.
- Track the close state yourself (listen for the 'close' event) and guard the .stream() call.
- If you only need events, register listeners once on a long-lived ChangeStream rather than re-deriving a Node stream.
Example fix
// before const cs = collection.watch(pipeline); await cs.close(); const nodeStream = cs.stream(); // throws // after const cs = collection.watch(pipeline); const nodeStream = cs.stream(); // obtain BEFORE closing
Defensive patterns
Strategy: type-guard
Validate before calling
// Before calling .stream(), guard on liveness
import { MongoChangeStreamError } from 'mongodb';
function streamIfOpen(cs) {
if (cs.closed) throw new MongoChangeStreamError('ChangeStream is closed');
return cs.stream();
} Type guard
// closed is a public readonly boolean on ChangeStream
function isOpen(cs): cs is import('mongodb').ChangeStream {
return !cs.closed;
} Try / catch
try {
const s = cs.stream();
} catch (err) {
if (err instanceof MongoChangeStreamError && err.message === 'ChangeStream is closed') {
// recreate the change stream instead
} else throw err;
} Prevention
- Obtain .stream() immediately after collection.watch() and before any close path.
- Track the 'close' event to invalidate cached references.
- Never reuse a ChangeStream across reconnect cycles; create a new one.
When it happens
Trigger: Calling changeStream.stream() after await changeStream.close(), after the 'close' event has fired, or after the stream auto-closed due to an unrecoverable error in emitter mode.
Common situations: Code that caches a ChangeStream reference and later calls .stream() without checking liveness; reuse of a stream across reconnect cycles; calling .stream() inside a 'close' or 'end' event handler.
Related errors
- This batch has already been executed, create new batch to ex
- ChangeStream cannot be used as an EventEmitter after being u
- ChangeStream cannot be used as an iterator after being used
- A change stream document has been received that lacks a resu
- Cursor is exhausted
AI-assisted analysis of mongodb/node-mongodb-native@3366c21a63 (2026-08-04).
Data as JSON: /data/errors/526748112b254bf1.json.
Report an issue: GitHub.