{"record":{"id":"274339719227a3c4","repo":"Automattic/mongoose","slug":"cannot-call-hasnext-on-errored-changestream","errorCode":null,"errorMessage":"Cannot call hasNext() on errored ChangeStream","messagePattern":"Cannot call hasNext\\(\\) on errored ChangeStream","errorType":"exception","errorClass":"MongooseError","httpStatus":null,"severity":"error","filePath":"lib/cursor/changeStream.js","lineNumber":101,"sourceCode":"    }\n\n    this.driverChangeStream.on('close', () => {\n      this.closed = true;\n    });\n\n    driverChangeStreamEvents.forEach(ev => {\n      this.driverChangeStream.on(ev, data => {\n        if (data?.fullDocument != null && this.options?.hydrate) {\n          data.fullDocument = this.options.model.hydrate(data.fullDocument);\n        }\n        this.emit(ev, data);\n      });\n    });\n  }\n\n  hasNext(cb) {\n    if (this.errored) {\n      throw new MongooseError('Cannot call hasNext() on errored ChangeStream');\n    }\n\n    if (this.driverChangeStream != null) {\n      return this.driverChangeStream.hasNext(cb);\n    }\n\n    return this.$driverChangeStreamPromise.then(\n      () => this.driverChangeStream.hasNext(cb),\n      err => {\n        if (cb != null) {\n          return cb(err);\n        }\n        throw err;\n      }\n    );\n  }\n\n  next(cb) {","sourceCodeStart":83,"sourceCodeEnd":119,"githubUrl":"https://github.com/Automattic/mongoose/blob/49cdab01366679723b487ecb754b38570f783289/lib/cursor/changeStream.js#L83-L119","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Attach an 'error' handler; on error, close() the dead stream and create a new one via Model.watch()","Guard calls: if (stream.errored || stream.closed) stream = await rebuildStream();","Wrap the stream lifecycle in a supervisor that recreates it with exponential backoff"],"exampleFix":"// before\nconst more = await stream.hasNext(); // throws after a failover\n\n// after\nstream.on('error', async err => {\n  await stream.close().catch(() => {});\n  stream = MyModel.watch(pipeline, opts); // recreate\n});\nconst more = stream.errored ? false : await stream.hasNext();","handlingStrategy":"validation","validationCode":"function usable(stream) {\n  return stream != null && !stream.errored && !stream.closed;\n}\nif (!usable(stream)) stream = await makeStream(); // rebuild via Model.watch()\nconst more = usable(stream) ? await stream.hasNext() : false;","typeGuard":"const isUsableChangeStream = (s) =>\n  s != null && typeof s.on === 'function' && !s.errored && !s.closed;","tryCatchPattern":"try {\n  await stream.hasNext();\n} catch (err) {\n  if (/errored ChangeStream/.test(err.message)) {\n    await stream.close().catch(() => {});\n    stream = await makeStream(); // recreate, then retry once\n  } else {\n    throw err;\n  }\n}","preventionTips":["Attach an 'error' listener immediately after Model.watch() — an unhandled 'error' event can crash the process","Treat change streams as disposable: rebuild on error instead of reusing","Centralize stream ownership in one module so recreation is not scattered across the app"],"tags":["change-stream","hasnext","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"}