Automattic/mongoose · error · MongooseError
Cannot call `next()` on a closed cursor
Error message
Cannot call `next()` on a closed cursor
What it means
QueryCursor sets _closed = true once close() succeeds. A subsequent next() throws this error because the underlying driver cursor is destroyed; Mongoose fails fast instead of hanging or serving stale data.
Source
Thrown at lib/cursor/queryCursor.js:312
});
return this;
};
/**
* Get the next document from this cursor. Will return `null` when there are
* no documents left.
*
* @return {Promise}
* @api public
* @method next
*/
QueryCursor.prototype.next = async function next() {
if (typeof arguments[0] === 'function') {
throw new MongooseError('QueryCursor.prototype.next() no longer accepts a callback');
}
if (this._closed) {
throw new MongooseError('Cannot call `next()` on a closed cursor');
}
const _this = this;
return cursorNextChannel.trace(function maybeTracedQueryCursorNext() {
return new Promise((resolve, reject) => {
_next(_this, function(error, doc) {
if (error) {
return reject(error);
}
resolve(doc);
});
});
}, () => ({
operation: _this.query.op || 'find',
collection: _this.query.mongooseCollection.name,
database: _this.model.db?.name,
serverAddress: _this.model.db?.host,
serverPort: _this.model.db?.port,
batchSize: _this.options.batchSize || _this.query.options?.batchSize,View on GitHub (pinned to 49cdab0136)
Solutions
- Stop iterating immediately after close() — return or break out of the read loop
- If more documents are needed later, create a fresh cursor by re-running the query: Model.find().cursor()
- Prefer letting the cursor drain naturally (next() resolves null) and only close() for early exit
Example fix
// before
const docs = [];
for (let i = 0; i < 3; i++) docs.push(await cursor.next());
await cursor.close();
const extra = await cursor.next(); // throws
// after
let doc; let n = 0;
while ((doc = await cursor.next()) !== null && n < 3) { docs.push(doc); n++; }
await cursor.close();
// need more later? create a fresh cursor
const fresh = Model.find().cursor(); Defensive patterns
Strategy: validation
Validate before calling
let closed = false;
async function closeCursor() {
closed = true;
await cursor.close();
}
async function safeNext() {
if (closed) return null; // treat as exhausted instead of calling next()
return cursor.next();
} Try / catch
try {
doc = await cursor.next();
} catch (err) {
if (/closed cursor/.test(err.message)) return null; // already closed: treat as end of stream
throw err;
} Prevention
- Give the cursor exactly one owner loop; never share it across concurrent consumers
- Track a closed flag in app code the moment close() is called
- Prefer eachAsync() for bounded processing — it handles exhaustion internally
When it happens
Trigger: const cur = Model.find().cursor(); await cur.close(); await cur.next(); — reading after close; also a race where one consumer closes the cursor while another keeps calling next().
Common situations: Early-exit loops that close the cursor and then fall through to one more next(); concurrent tasks sharing a single cursor; cleanup or timeout handlers racing with iteration.
Related errors
- Arguments must be aggregate pipeline operators
- Invalid addFields() argument. Must be an object
- Invalid project() argument. Must be string or object
- Aggregate `near()` must be called with non-nullish argument
- Aggregate `near()` argument must have a `near` property
AI-assisted analysis of Automattic/mongoose@49cdab0136 (2026-08-21).
Data as JSON: /api/errors/b2feaf98cf2e15aa.
Report an issue: GitHub.