{"record":{"id":"cca1bb84f05c935a","repo":"Automattic/mongoose","slug":"cannot-call-next-on-errored-changestream","errorCode":null,"errorMessage":"Cannot call next() on errored ChangeStream","messagePattern":"Cannot call next\\(\\) on errored ChangeStream","errorType":"exception","errorClass":"MongooseError","httpStatus":null,"severity":"error","filePath":"lib/cursor/changeStream.js","lineNumber":121,"sourceCode":"\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) {\n    if (this.errored) {\n      throw new MongooseError('Cannot call next() on errored ChangeStream');\n    }\n    if (this.options?.hydrate) {\n      if (cb != null) {\n        const originalCb = cb;\n        cb = (err, data) => {\n          if (err != null) {\n            return originalCb(err);\n          }\n          if (data.fullDocument != null) {\n            data.fullDocument = this.options.model.hydrate(data.fullDocument);\n          }\n          return originalCb(null, data);\n        };\n      }\n\n      let maybePromise;\n      if (this.driverChangeStream != null) {\n        maybePromise = this.driverChangeStream.next(cb);","sourceCodeStart":103,"sourceCodeEnd":139,"githubUrl":"https://github.com/Automattic/mongoose/blob/49cdab01366679723b487ecb754b38570f783289/lib/cursor/changeStream.js#L103-L139","documentation":"ChangeStream sets this.errored when the underlying driver stream fails fatally (connection loss, topology change, unrecoverable resume error). Calling next() on an errored stream throws immediately: the stream cannot produce further change documents and Mongoose fails fast rather than hanging.","triggerScenarios":"Calling stream.next() (with or without a callback) after the change stream emitted an 'error' event, for example after a primary failover or network interruption.","commonSituations":"Poll-style consumers that loop on next(); reconnect logic that keeps draining a stream that already died; MongoDB restarts or network blits between app and cluster.","solutions":["Listen for the 'error' event and rebuild: close() the stream, then create a fresh one with Model.watch()","Check stream.errored (and stream.closed) before each next() call","For resilient consumers, run the stream under a supervisor that recreates it with backoff"],"exampleFix":"// before\nconst change = await stream.next(); // throws once the stream has errored\n\n// after\nstream.on('error', async err => {\n  await stream.close().catch(() => {});\n  stream = MyModel.watch(pipeline, opts);\n});\nconst change = (stream.errored || stream.closed) ? null : await stream.next();","handlingStrategy":"validation","validationCode":"function usable(stream) {\n  return stream != null && !stream.errored && !stream.closed;\n}\nif (!usable(stream)) stream = await makeStream();\nconst change = usable(stream) ? await stream.next() : null;","typeGuard":"const isUsableChangeStream = (s) =>\n  s != null && typeof s.on === 'function' && !s.errored && !s.closed;","tryCatchPattern":"try {\n  change = await stream.next();\n} catch (err) {\n  if (/errored ChangeStream/.test(err.message)) {\n    await stream.close().catch(() => {});\n    stream = await makeStream();\n    change = await stream.next(); // one retry on the fresh stream\n  } else {\n    throw err;\n  }\n}","preventionTips":["Check the errored flag before every next() in polling loops","Listen for 'error' and rebuild the stream rather than retrying on the dead one","Add jittered backoff around stream recreation to survive cluster instability"],"tags":["change-stream","next","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"}