mongodb/node-mongodb-native · error · MongoRuntimeError

Unable to iterate cursor with no id

Error message

Unable to iterate cursor with no id

What it means

Thrown by GetMoreOperation.buildCommand when the cursor being iterated has a null or zero cursor id. A zero id means no server-side cursor exists - either the initial batch already exhausted the result set, the cursor was already closed, or it was never successfully created. This is a MongoRuntimeError indicating invalid driver/cursor state rather than a bad user argument.

Source

Thrown at src/operations/get_more.ts:58

  cursorId: Long;
  override options: GetMoreOptions;

  constructor(ns: MongoDBNamespace, cursorId: Long, server: Server, options: GetMoreOptions) {
    super(options);

    this.options = options;
    this.ns = ns;
    this.cursorId = cursorId;
    this.server = server;
  }

  override get commandName() {
    return 'getMore' as const;
  }

  override buildCommand(_connection: Connection): Document {
    if (this.cursorId == null || this.cursorId.isZero()) {
      throw new MongoRuntimeError('Unable to iterate cursor with no id');
    }

    const collection = this.ns.collection;
    if (collection == null) {
      // Cursors should have adopted the namespace returned by MongoDB
      // which should always defined a collection name (even a pseudo one, ex. db.aggregate())
      throw new MongoRuntimeError('A collection name must be determined before getMore');
    }

    const getMoreCmd: GetMoreCommand = {
      getMore: this.cursorId,
      collection
    };

    if (typeof this.options.batchSize === 'number') {
      getMoreCmd.batchSize = Math.abs(this.options.batchSize);
    }

View on GitHub (pinned to 3366c21a63)

Solutions

  1. Do not reuse a cursor after exhaustion - re-issue the query/find/aggregate to obtain a fresh cursor.
  2. Ensure you fully consume a cursor exactly once (single toArray()/for-await loop).
  3. If iterating in pages, capture the cursor once and drive it linearly; don't hand the same cursor to concurrent consumers.
  4. On transient errors, restart the whole operation (new find/aggregate) rather than resuming the dead cursor.

Example fix

// before - reusing an exhausted cursor
cursor = collection.find({});
await cursor.toArray();
await cursor.next(); // throws: no id
// after - create a new cursor for each iteration
await collection.find({}).toArray();
Defensive patterns

Strategy: retry

Try / catch

try {
  await cursor.next();
} catch (e) {
  if (e instanceof MongoRuntimeError && /no id/.test(e.message)) {
    // cursor is exhausted/dead - re-run the query to get a fresh cursor instead of resuming
    cursor = collection.find(filter);
  } else throw e;
}

Prevention

When it happens

Trigger: Iterating a cursor a second time after it was fully consumed or explicitly closed; iterating a cursor whose initial command returned id 0 (small result set returned in the first batch); a race where killCursors ran between batches; calling .next()/.toArray() on a cursor after an error already tore it down; manually reusing a cursor object across async boundaries.

Common situations: Storing a cursor in a shared variable and iterating it from two places; awaiting toArray() then calling toArray() again; tailable/change-stream cursors during shutdown; retry logic that re-enters a cursor loop after a transient failure without re-creating the cursor.

Related errors


AI-assisted analysis of mongodb/node-mongodb-native@3366c21a63 (2026-08-04). Data as JSON: /data/errors/b5ba08685a791729.json. Report an issue: GitHub.