Automattic/mongoose · error · MongooseError
Cannot call hasNext() on errored ChangeStream
Error message
Cannot call hasNext() on errored ChangeStream
What it means
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.
Source
Thrown at lib/cursor/changeStream.js:101
}
this.driverChangeStream.on('close', () => {
this.closed = true;
});
driverChangeStreamEvents.forEach(ev => {
this.driverChangeStream.on(ev, data => {
if (data?.fullDocument != null && this.options?.hydrate) {
data.fullDocument = this.options.model.hydrate(data.fullDocument);
}
this.emit(ev, data);
});
});
}
hasNext(cb) {
if (this.errored) {
throw new MongooseError('Cannot call hasNext() on errored ChangeStream');
}
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) {View on GitHub (pinned to 49cdab0136)
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
Example fix
// before
const more = await stream.hasNext(); // throws after a failover
// after
stream.on('error', async err => {
await stream.close().catch(() => {});
stream = MyModel.watch(pipeline, opts); // recreate
});
const more = stream.errored ? false : await stream.hasNext(); Defensive patterns
Strategy: validation
Validate before calling
function usable(stream) {
return stream != null && !stream.errored && !stream.closed;
}
if (!usable(stream)) stream = await makeStream(); // rebuild via Model.watch()
const more = usable(stream) ? await stream.hasNext() : false; Type guard
const isUsableChangeStream = (s) => s != null && typeof s.on === 'function' && !s.errored && !s.closed;
Try / catch
try {
await stream.hasNext();
} catch (err) {
if (/errored ChangeStream/.test(err.message)) {
await stream.close().catch(() => {});
stream = await makeStream(); // recreate, then retry once
} else {
throw err;
}
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Cannot call next() 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/274339719227a3c4.
Report an issue: GitHub.