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

  1. Stop iterating immediately after close() — return or break out of the read loop
  2. If more documents are needed later, create a fresh cursor by re-running the query: Model.find().cursor()
  3. 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

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


AI-assisted analysis of Automattic/mongoose@49cdab0136 (2026-08-21). Data as JSON: /api/errors/b2feaf98cf2e15aa. Report an issue: GitHub.