mongodb/node-mongodb-native · error · MongoCursorExhaustedError

Cursor is exhausted

Error message

Cursor is exhausted

What it means

Thrown as a MongoCursorExhaustedError (default message 'Cursor is exhausted') from next() when cursorId equals Long.ZERO at entry. A zero cursor id means the server already returned all results in the firstBatch and the cursor is closed server-side, so there is nothing more to fetch. next() is meant to return null for end-of-results during normal iteration, but calling it again after the cursor is already known-exhausted is treated as a logic error. The same guard exists in tryNext() at line 585.

Solutions

  1. Treat a `null` return from next() as end-of-stream and stop calling next().
  2. Prefer `for await (const doc of cursor)` which handles exhaustion cleanly.
  3. If you need repeat reads, call cursor.rewind() (when allowed) or create a fresh cursor via collection.find() again.
  4. Guard with `if (!cursor.closed)` before manual next() calls.

Example fix

// before: keeps calling next() past exhaustion
while (true) {
  const doc = await cursor.next(); // throws on the call after null
}

// after: stop on null (or just use for-await)
let doc;
while ((doc = await cursor.next()) !== null) {
  handle(doc);
}
Defensive patterns

Strategy: validation

Validate before calling

// Stop calling next() once the cursor is exhausted
if (!cursor.closed) {
  const doc = await cursor.next();
  if (doc === null) return; // end of stream
}

Try / catch

try {
  const doc = await cursor.next();
} catch (err) {
  if (err instanceof MongoCursorExhaustedError) {
    // cursor already drained; stop iterating
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling cursor.next() after a previous next()/hasNext()/for-await loop already drained the cursor and set cursorId to Long.ZERO; calling next() on a cursor whose initial command returned a zero id and a full firstBatch.

Common situations: Manual next() loops without checking the return value for null; sharing a cursor across consumers where one finishes it and another calls next(); reusing a cursor stored in a long-lived variable after it has been fully read.

Related errors


AI-assisted analysis of mongodb/node-mongodb-native@dce7939f86 (2026-08-11). Data as JSON: /api/errors/7640f1cff3fa88d3. Report an issue: GitHub.

Appendix: source

Thrown at src/cursor/abstract_cursor.ts:553

          return true;
        }
        await this.fetchBatch();
      } while (!this.isDead || (this.documents?.length ?? 0) !== 0);
    } finally {
      if (this.cursorOptions.timeoutMode === CursorTimeoutMode.ITERATION) {
        this.timeoutContext?.clear();
      }
    }

    return false;
  }

  /** Get the next available document from the cursor, returns null if no more documents are available. */
  async next(): Promise<TSchema | null> {
    this.signal?.throwIfAborted();

    if (this.cursorId === Long.ZERO) {
      throw new MongoCursorExhaustedError();
    }

    if (this.cursorOptions.timeoutMode === CursorTimeoutMode.ITERATION && this.cursorId != null) {
      this.timeoutContext?.refresh();
    }

    try {
      do {
        const doc = this.documents?.shift(this.deserializationOptions);
        if (doc != null) {
          if (this.transform != null) return await this.transformDocument(doc);
          return doc;
        }
        await this.fetchBatch();
      } while (!this.isDead || (this.documents?.length ?? 0) !== 0);
    } finally {
      if (this.cursorOptions.timeoutMode === CursorTimeoutMode.ITERATION) {
        this.timeoutContext?.clear();

View on GitHub (pinned to dce7939f86)