{"record":{"id":"0f37f724e2def207","repo":"Automattic/mongoose","slug":"cannot-create-change-stream-with-hydrate-true-u","errorCode":null,"errorMessage":"Cannot create change stream with `hydrate: true` unless calling `Model.watch()`","messagePattern":"Cannot create change stream with `hydrate: true` unless calling `Model\\.watch\\(\\)`","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"lib/cursor/changeStream.js","lineNumber":32,"sourceCode":"const driverChangeStreamEvents = ['close', 'change', 'end', 'error', 'resumeTokenChanged'];\n\n/*!\n * ignore\n */\n\nclass ChangeStream extends EventEmitter {\n  constructor(changeStreamPromise, pipeline, options) {\n    super();\n\n    this.driverChangeStream = null;\n    this.closed = false;\n    this.bindedEvents = false;\n    this.pipeline = pipeline;\n    this.options = options;\n    this.errored = false;\n\n    if (options?.hydrate && !options.model) {\n      throw new Error(\n        'Cannot create change stream with `hydrate: true` ' +\n        'unless calling `Model.watch()`'\n      );\n    }\n\n    this.$driverChangeStreamPromise = changeStreamPromise.then(\n      driverChangeStream => {\n        this.driverChangeStream = driverChangeStream;\n        // Use setImmediate so the stream pump (_read) has a chance to run and\n        // the driver cursor initializes before 'ready' resolves. Without this,\n        // changes emitted immediately after 'ready' can be missed because the\n        // underlying cursor hasn't sent its initial aggregate to MongoDB yet.\n        setImmediate(() => this.emit('ready'));\n        return this;\n      },\n      err => {\n        this.errored = true;\n        this.emit('error', err);","sourceCodeStart":14,"sourceCodeEnd":50,"githubUrl":"https://github.com/Automattic/mongoose/blob/49cdab01366679723b487ecb754b38570f783289/lib/cursor/changeStream.js#L14-L50","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Create hydrated streams through Model.watch(pipeline, { hydrate: true }) so the model is supplied automatically","If you must build the stream yourself, include the model: { hydrate: true, model: MyModel }","Drop hydrate: true when raw change events (plain objects) are wanted and no hydration is needed"],"exampleFix":"// before\nconst stream = someInternalWatch(pipeline, { hydrate: true }); // no model in options\n\n// after\nconst stream = MyModel.watch(pipeline, { hydrate: true });","handlingStrategy":"validation","validationCode":"function watchWithHydrate(Model, pipeline, options = {}) {\n  // Model.watch() injects the model, which hydrate: true requires\n  return Model.watch(pipeline, { ...options, hydrate: true });\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always create change streams via Model.watch() — never construct ChangeStream directly","In generic wrappers, reject options.hydrate unless a Model is available to inject","Treat $-prefixed and constructor-level internals as off-limits in application code"],"tags":["change-stream","watch","hydrate","mongoose"],"backgroundTag":"change-stream-options-invalid","analyzedSha":"49cdab01366679723b487ecb754b38570f783289","analyzedAt":"2026-08-21T22:54:00.882Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}