Automattic/mongoose · error · MongooseError

Cannot call next() on errored ChangeStream

Error message

Cannot call next() on errored ChangeStream

What it means

ChangeStream sets this.errored when the underlying driver stream fails fatally (connection loss, topology change, unrecoverable resume error). Calling next() on an errored stream throws immediately: the stream cannot produce further change documents and Mongoose fails fast rather than hanging.

Source

Thrown at lib/cursor/changeStream.js:121

    if (this.driverChangeStream != null) {
      return this.driverChangeStream.hasNext(cb);
    }

    return this.$driverChangeStreamPromise.then(
      () => this.driverChangeStream.hasNext(cb),
      err => {
        if (cb != null) {
          return cb(err);
        }
        throw err;
      }
    );
  }

  next(cb) {
    if (this.errored) {
      throw new MongooseError('Cannot call next() on errored ChangeStream');
    }
    if (this.options?.hydrate) {
      if (cb != null) {
        const originalCb = cb;
        cb = (err, data) => {
          if (err != null) {
            return originalCb(err);
          }
          if (data.fullDocument != null) {
            data.fullDocument = this.options.model.hydrate(data.fullDocument);
          }
          return originalCb(null, data);
        };
      }

      let maybePromise;
      if (this.driverChangeStream != null) {
        maybePromise = this.driverChangeStream.next(cb);

View on GitHub (pinned to 49cdab0136)

Solutions

  1. Listen for the 'error' event and rebuild: close() the stream, then create a fresh one with Model.watch()
  2. Check stream.errored (and stream.closed) before each next() call
  3. For resilient consumers, run the stream under a supervisor that recreates it with backoff

Example fix

// before
const change = await stream.next(); // throws once the stream has errored

// after
stream.on('error', async err => {
  await stream.close().catch(() => {});
  stream = MyModel.watch(pipeline, opts);
});
const change = (stream.errored || stream.closed) ? null : await stream.next();
Defensive patterns

Strategy: validation

Validate before calling

function usable(stream) {
  return stream != null && !stream.errored && !stream.closed;
}
if (!usable(stream)) stream = await makeStream();
const change = usable(stream) ? await stream.next() : null;

Type guard

const isUsableChangeStream = (s) =>
  s != null && typeof s.on === 'function' && !s.errored && !s.closed;

Try / catch

try {
  change = await stream.next();
} catch (err) {
  if (/errored ChangeStream/.test(err.message)) {
    await stream.close().catch(() => {});
    stream = await makeStream();
    change = await stream.next(); // one retry on the fresh stream
  } else {
    throw err;
  }
}

Prevention

When it happens

Trigger: Calling stream.next() (with or without a callback) after the change stream emitted an 'error' event, for example after a primary failover or network interruption.

Common situations: Poll-style consumers that loop on next(); reconnect logic that keeps draining a stream that already died; MongoDB restarts or network blits between app and cluster.

Related errors


AI-assisted analysis of Automattic/mongoose@49cdab0136 (2026-08-21). Data as JSON: /api/errors/cca1bb84f05c935a. Report an issue: GitHub.