mongodb/node-mongodb-native · error · MongoAPIError

ChangeStream cannot be used as an EventEmitter after being u

Error message

ChangeStream cannot be used as an EventEmitter after being used as an iterator

What it means

A ChangeStream can be consumed in exactly one mode: EventEmitter (on('change', ...)) or async iterator (for-await). The internal _setIsEmitter() throws this MongoAPIError when you attach event listeners after the stream has already started being consumed with for-await-of. The mode is sticky and mutually exclusive to prevent two concurrent consumption loops racing on the same cursor.

Source

Thrown at src/change_stream.ts:880

   *
   * 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 {
    if (this.mode === 'emitter') {
      // TODO(NODE-3485): Replace with MongoChangeStreamModeError
      throw new MongoAPIError(
        'ChangeStream cannot be used as an iterator after being used as an EventEmitter'
      );
    }
    this.mode = 'iterator';
  }

  /**

View on GitHub (pinned to 3366c21a63)

Solutions

  1. Pick one consumption style per ChangeStream instance and remove the other.
  2. If you need both behaviors, create two separate ChangeStream instances via two collection.watch() calls.
  3. Refactor so iteration results are pushed into your own EventEmitter rather than reusing the driver's.

Example fix

// before
for await (const change of cs) { handle(change); }
cs.on('change', handle); // throws

// after
for await (const change of cs) { handle(change); }
Defensive patterns

Strategy: validation

Validate before calling

// Enforce one consumption mode per instance at the call site
function assertEmitterMode(cs) {
  if (cs.isClosed || /* iterator started */ false) throw new Error('already iterating');
}

Type guard

type ChangeStreamMode = 'emitter' | 'iterator' | null;
// Mode is internal; treat it as an application-level invariant: one style per instance.

Try / catch

try {
  cs.on('change', handler);
} catch (err) {
  if (err instanceof MongoAPIError && /EventEmitter after being used as an iterator/.test(err.message)) {
    // create a new ChangeStream for emitter use
  } else throw err;
}

Prevention

When it happens

Trigger: Running `for await (const change of changeStream)` and then, later, calling `changeStream.on('change', ...)` (or any event listener registration that triggers _streamEvents) on the same instance.

Common situations: Migrating code from event style to iterator style piecemeal; mixing a helper that iterates with another that listens; refactoring that leaves a stray .on('change') call after introducing a for-await loop.

Related errors


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