Automattic/mongoose · error · MongooseError
Cannot call next() on errored ChangeStream
Error message
Cannot call next() on errored ChangeStream
What it means
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.
Source
Thrown at lib/cursor/changeStream.js:121
if (this.driverChangeStream != null) {
return this.driverChangeStream.hasNext(cb);
}
return this.$driverChangeStreamPromise.then(
() => this.driverChangeStream.hasNext(cb),
err => {
if (cb != null) {
return cb(err);
}
throw err;
}
);
}
next(cb) {
if (this.errored) {
throw new MongooseError('Cannot call next() on errored ChangeStream');
}
if (this.options?.hydrate) {
if (cb != null) {
const originalCb = cb;
cb = (err, data) => {
if (err != null) {
return originalCb(err);
}
if (data.fullDocument != null) {
data.fullDocument = this.options.model.hydrate(data.fullDocument);
}
return originalCb(null, data);
};
}
let maybePromise;
if (this.driverChangeStream != null) {
maybePromise = this.driverChangeStream.next(cb);View on GitHub (pinned to 49cdab0136)
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
Example fix
// before
const change = await stream.next(); // throws once the stream has errored
// after
stream.on('error', async err => {
await stream.close().catch(() => {});
stream = MyModel.watch(pipeline, opts);
});
const change = (stream.errored || stream.closed) ? null : await stream.next(); Defensive patterns
Strategy: validation
Validate before calling
function usable(stream) {
return stream != null && !stream.errored && !stream.closed;
}
if (!usable(stream)) stream = await makeStream();
const change = usable(stream) ? await stream.next() : null; Type guard
const isUsableChangeStream = (s) => s != null && typeof s.on === 'function' && !s.errored && !s.closed;
Try / catch
try {
change = await stream.next();
} catch (err) {
if (/errored ChangeStream/.test(err.message)) {
await stream.close().catch(() => {});
stream = await makeStream();
change = await stream.next(); // one retry on the fresh stream
} else {
throw err;
}
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Cannot call hasNext() on errored ChangeStream
- Cannot call addListener() on errored ChangeStream
- Cannot call on() on errored ChangeStream
- Cannot call once() on errored ChangeStream
- Cannot create change stream with `hydrate: true` unless call
AI-assisted analysis of Automattic/mongoose@49cdab0136 (2026-08-21).
Data as JSON: /api/errors/cca1bb84f05c935a.
Report an issue: GitHub.