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
- Treat a `null` return from next() as end-of-stream and stop calling next().
- Prefer `for await (const doc of cursor)` which handles exhaustion cleanly.
- If you need repeat reads, call cursor.rewind() (when allowed) or create a fresh cursor via collection.find() again.
- 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
- Prefer for-await, which stops cleanly at exhaustion.
- Treat a null return from next() as end-of-stream and stop.
- Do not share a single cursor across multiple consumers.
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
- Cursor is already initialized
- Cursor returned a `null` document, but the cursor is not…
- Unable to iterate cursor with no id
- A collection name must be determined before getMore
- A collection name must be determined before killCursors
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)