{"record":{"id":"c1c0342af4d13de1","repo":"Automattic/mongoose","slug":"cannot-call-on-on-errored-changestream","errorCode":null,"errorMessage":"Cannot call on() on errored ChangeStream","messagePattern":"Cannot call on\\(\\) on errored ChangeStream","errorType":"exception","errorClass":"MongooseError","httpStatus":null,"severity":"error","filePath":"lib/cursor/changeStream.js","lineNumber":187,"sourceCode":"        if (cb != null) {\n          return cb(err);\n        }\n        throw err;\n      }\n    );\n  }\n\n  addListener(event, handler) {\n    if (this.errored) {\n      throw new MongooseError('Cannot call addListener() on errored ChangeStream');\n    }\n    this._bindEvents();\n    return super.addListener(event, handler);\n  }\n\n  on(event, handler) {\n    if (this.errored) {\n      throw new MongooseError('Cannot call on() on errored ChangeStream');\n    }\n    this._bindEvents();\n    return super.on(event, handler);\n  }\n\n  once(event, handler) {\n    if (this.errored) {\n      throw new MongooseError('Cannot call once() on errored ChangeStream');\n    }\n    this._bindEvents();\n    return super.once(event, handler);\n  }\n\n  close() {\n    this.closed = true;\n    if (this.driverChangeStream) {\n      return this.driverChangeStream.close();\n    } else {","sourceCodeStart":169,"sourceCodeEnd":205,"githubUrl":"https://github.com/Automattic/mongoose/blob/49cdab01366679723b487ecb754b38570f783289/lib/cursor/changeStream.js#L169-L205","documentation":"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.","triggerScenarios":"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.","commonSituations":"Event-driven consumers re-attaching 'change'/'error' handlers after failovers; plugin lifecycle code calling on() during teardown/restart of a watcher.","solutions":["Handle 'error' by closing the dead stream and creating a new one, then attach handlers to the new instance","Guard: if (stream.errored || stream.closed) stream = await rebuildStream(); before stream.on(...)","Centralize stream creation in one owner so recreation and re-attachment happen together"],"exampleFix":"// before\nstream.on('change', sync); // re-registering after a failure throws\n\n// after\nstream.on('error', async err => {\n  await stream.close().catch(() => {});\n  stream = MyModel.watch(pipeline, opts);\n  stream.on('change', sync);\n});","handlingStrategy":"validation","validationCode":"function onSafe(stream, event, handler) {\n  if (stream.errored || stream.closed) stream = makeStream();\n  stream.on(event, handler);\n  return stream;\n}","typeGuard":"const isUsableChangeStream = (s) =>\n  s != null && typeof s.on === 'function' && !s.errored && !s.closed;","tryCatchPattern":null,"preventionTips":["Attach an 'error' handler as the first listener on every change stream","Recreate streams on fatal errors instead of re-registering handlers on them","Log stream.errored state transitions to catch failover-related breakage early"],"tags":["change-stream","event-listener","error-recovery","mongoose"],"backgroundTag":"change-stream-resume-failed","analyzedSha":"49cdab01366679723b487ecb754b38570f783289","analyzedAt":"2026-08-21T22:54:00.882Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}