Automattic/mongoose · error · MongooseError

Cannot call on() on errored ChangeStream

Error message

Cannot call on() on errored ChangeStream

What it means

ChangeStream sets this.errored after a fatal driver-level failure. on() against an errored stream throws immediately: handlers attached to a dead stream would never fire, so Mongoose treats re-registration as a bug in the caller.

Source

Thrown at lib/cursor/changeStream.js:187

        if (cb != null) {
          return cb(err);
        }
        throw err;
      }
    );
  }

  addListener(event, handler) {
    if (this.errored) {
      throw new MongooseError('Cannot call addListener() on errored ChangeStream');
    }
    this._bindEvents();
    return super.addListener(event, handler);
  }

  on(event, handler) {
    if (this.errored) {
      throw new MongooseError('Cannot call on() on errored ChangeStream');
    }
    this._bindEvents();
    return super.on(event, handler);
  }

  once(event, handler) {
    if (this.errored) {
      throw new MongooseError('Cannot call once() on errored ChangeStream');
    }
    this._bindEvents();
    return super.once(event, handler);
  }

  close() {
    this.closed = true;
    if (this.driverChangeStream) {
      return this.driverChangeStream.close();
    } else {

View on GitHub (pinned to 49cdab0136)

Solutions

  1. Handle 'error' by closing the dead stream and creating a new one, then attach handlers to the new instance
  2. Guard: if (stream.errored || stream.closed) stream = await rebuildStream(); before stream.on(...)
  3. Centralize stream creation in one owner so recreation and re-attachment happen together

Example fix

// before
stream.on('change', sync); // re-registering after a failure throws

// after
stream.on('error', async err => {
  await stream.close().catch(() => {});
  stream = MyModel.watch(pipeline, opts);
  stream.on('change', sync);
});
Defensive patterns

Strategy: validation

Validate before calling

function onSafe(stream, event, handler) {
  if (stream.errored || stream.closed) stream = makeStream();
  stream.on(event, handler);
  return stream;
}

Type guard

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

Prevention

When it happens

Trigger: Calling stream.on('change', fn) after the stream has emitted a fatal 'error' — common in reconnection code paths or when listener setup happens lazily and races with a failure.

Common situations: Event-driven consumers re-attaching 'change'/'error' handlers after failovers; plugin lifecycle code calling on() during teardown/restart of a watcher.

Related errors


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