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

  1. Create a fresh ChangeStream via collection.watch()/db.watch() before calling .stream() instead of reusing the closed one.
  2. Track the close state yourself (listen for the 'close' event) and guard the .stream() call.
  3. 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

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


AI-assisted analysis of mongodb/node-mongodb-native@3366c21a63 (2026-08-04). Data as JSON: /data/errors/526748112b254bf1.json. Report an issue: GitHub.