Automattic/mongoose · error · MongooseError

Cannot call hasNext() on errored ChangeStream

Error message

Cannot call hasNext() on errored ChangeStream

What it means

ChangeStream tracks an errored flag that is set when the underlying driver change stream fails fatally (network drop, topology change, unrecoverable resume failure). Once errored, Mongoose refuses hasNext() by throwing immediately, because the stream can no longer deliver events.

Source

Thrown at lib/cursor/changeStream.js:101

    }

    this.driverChangeStream.on('close', () => {
      this.closed = true;
    });

    driverChangeStreamEvents.forEach(ev => {
      this.driverChangeStream.on(ev, data => {
        if (data?.fullDocument != null && this.options?.hydrate) {
          data.fullDocument = this.options.model.hydrate(data.fullDocument);
        }
        this.emit(ev, data);
      });
    });
  }

  hasNext(cb) {
    if (this.errored) {
      throw new MongooseError('Cannot call hasNext() on errored ChangeStream');
    }

    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) {

View on GitHub (pinned to 49cdab0136)

Solutions

  1. Attach an 'error' handler; on error, close() the dead stream and create a new one via Model.watch()
  2. Guard calls: if (stream.errored || stream.closed) stream = await rebuildStream();
  3. Wrap the stream lifecycle in a supervisor that recreates it with exponential backoff

Example fix

// before
const more = await stream.hasNext(); // throws after a failover

// after
stream.on('error', async err => {
  await stream.close().catch(() => {});
  stream = MyModel.watch(pipeline, opts); // recreate
});
const more = stream.errored ? false : await stream.hasNext();
Defensive patterns

Strategy: validation

Validate before calling

function usable(stream) {
  return stream != null && !stream.errored && !stream.closed;
}
if (!usable(stream)) stream = await makeStream(); // rebuild via Model.watch()
const more = usable(stream) ? await stream.hasNext() : false;

Type guard

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

Try / catch

try {
  await stream.hasNext();
} catch (err) {
  if (/errored ChangeStream/.test(err.message)) {
    await stream.close().catch(() => {});
    stream = await makeStream(); // recreate, then retry once
  } else {
    throw err;
  }
}

Prevention

When it happens

Trigger: Calling stream.hasNext() (with or without a callback) after the stream emitted an 'error' event — e.g. after a MongoDB failover, network drop, or resume-token expiry.

Common situations: Long-lived listeners (cache invalidation, audit logs) that outlive replica-set failovers; retry logic that calls hasNext() on the same dead stream instead of rebuilding it.

Related errors


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