Automattic/mongoose · error · Error

Cannot create change stream with `hydrate: true` unless call

Error message

Cannot create change stream with `hydrate: true` unless calling `Model.watch()`

What it means

ChangeStream can only hydrate fullDocument into Mongoose documents when it knows which model to use. Model.watch() injects that model into the options; constructing a ChangeStream any other way with hydrate: true but no model is a programming error, and the constructor throws immediately.

Source

Thrown at lib/cursor/changeStream.js:32

const driverChangeStreamEvents = ['close', 'change', 'end', 'error', 'resumeTokenChanged'];

/*!
 * ignore
 */

class ChangeStream extends EventEmitter {
  constructor(changeStreamPromise, pipeline, options) {
    super();

    this.driverChangeStream = null;
    this.closed = false;
    this.bindedEvents = false;
    this.pipeline = pipeline;
    this.options = options;
    this.errored = false;

    if (options?.hydrate && !options.model) {
      throw new Error(
        'Cannot create change stream with `hydrate: true` ' +
        'unless calling `Model.watch()`'
      );
    }

    this.$driverChangeStreamPromise = changeStreamPromise.then(
      driverChangeStream => {
        this.driverChangeStream = driverChangeStream;
        // Use setImmediate so the stream pump (_read) has a chance to run and
        // the driver cursor initializes before 'ready' resolves. Without this,
        // changes emitted immediately after 'ready' can be missed because the
        // underlying cursor hasn't sent its initial aggregate to MongoDB yet.
        setImmediate(() => this.emit('ready'));
        return this;
      },
      err => {
        this.errored = true;
        this.emit('error', err);

View on GitHub (pinned to 49cdab0136)

Solutions

  1. Create hydrated streams through Model.watch(pipeline, { hydrate: true }) so the model is supplied automatically
  2. If you must build the stream yourself, include the model: { hydrate: true, model: MyModel }
  3. Drop hydrate: true when raw change events (plain objects) are wanted and no hydration is needed

Example fix

// before
const stream = someInternalWatch(pipeline, { hydrate: true }); // no model in options

// after
const stream = MyModel.watch(pipeline, { hydrate: true });
Defensive patterns

Strategy: validation

Validate before calling

function watchWithHydrate(Model, pipeline, options = {}) {
  // Model.watch() injects the model, which hydrate: true requires
  return Model.watch(pipeline, { ...options, hydrate: true });
}

Prevention

When it happens

Trigger: Directly instantiating Mongoose's ChangeStream class (new ChangeStream(promise, pipeline, { hydrate: true })); a plugin or helper that calls collection-level watch plumbing with { hydrate: true } but no model option.

Common situations: Plugins that wrap or re-create change streams; code copied from the native driver's collection.watch() then extended with Mongoose options; generic watch wrappers that forward an options object without injecting the model.

Related errors


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