{"id":"7640f1cff3fa88d3","repo":"mongodb/node-mongodb-native","slug":"cursor-is-exhausted","errorCode":null,"errorMessage":"Cursor is exhausted","messagePattern":"Cursor is exhausted","errorType":"exception","errorClass":"MongoCursorExhaustedError","httpStatus":null,"severity":"error","filePath":"src/cursor/abstract_cursor.ts","lineNumber":553,"sourceCode":"          return true;\n        }\n        await this.fetchBatch();\n      } while (!this.isDead || (this.documents?.length ?? 0) !== 0);\n    } finally {\n      if (this.cursorOptions.timeoutMode === CursorTimeoutMode.ITERATION) {\n        this.timeoutContext?.clear();\n      }\n    }\n\n    return false;\n  }\n\n  /** Get the next available document from the cursor, returns null if no more documents are available. */\n  async next(): Promise<TSchema | null> {\n    this.signal?.throwIfAborted();\n\n    if (this.cursorId === Long.ZERO) {\n      throw new MongoCursorExhaustedError();\n    }\n\n    if (this.cursorOptions.timeoutMode === CursorTimeoutMode.ITERATION && this.cursorId != null) {\n      this.timeoutContext?.refresh();\n    }\n\n    try {\n      do {\n        const doc = this.documents?.shift(this.deserializationOptions);\n        if (doc != null) {\n          if (this.transform != null) return await this.transformDocument(doc);\n          return doc;\n        }\n        await this.fetchBatch();\n      } while (!this.isDead || (this.documents?.length ?? 0) !== 0);\n    } finally {\n      if (this.cursorOptions.timeoutMode === CursorTimeoutMode.ITERATION) {\n        this.timeoutContext?.clear();","sourceCodeStart":535,"sourceCodeEnd":571,"githubUrl":"https://github.com/mongodb/node-mongodb-native/blob/3366c21a6311e02f1be91da982f9b93d3cce99a0/src/cursor/abstract_cursor.ts#L535-L571","documentation":"MongoCursorExhaustedError thrown by cursor.next() when cursorId === Long.ZERO, meaning the server has already signaled that the cursor is fully drained and no more batches are forthcoming. Calling next() on an already-exhausted cursor is a logic error because there is nothing left to fetch.","triggerScenarios":"Calling await cursor.next() after iteration completed, after a previous next() returned null, after cursor.close(), or after the server returned a zero cursorId on the initial find/getMore. Common in polling loops that do not break on null.","commonSituations":"While/for loops that ignore a previous null return and call next() again; reusing a cursor stored in a long-lived object after it drained; race where two consumers drain the same cursor.","solutions":["Stop calling next() once it returns null — that signals exhaustion.","Use `for await (const doc of cursor)` which handles termination cleanly.","Track a local `exhausted` flag and skip further next() calls."],"exampleFix":"// before\nwhile (true) { const d = await cursor.next(); use(d); } // throws after drain\n// after\nfor await (const d of cursor) { use(d); }","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  const doc = await cursor.next();\n  if (doc === null) return; // graceful end\n} catch (e) {\n  if (e instanceof MongoCursorExhaustedError) return;\n  throw e;\n}","preventionTips":["Prefer for-await-of which terminates cleanly on exhaustion.","Treat a null return from next() as final — do not call next() again.","Track a local exhausted flag in manual loops."],"tags":["cursor","lifecycle","iteration"],"analyzedSha":"3366c21a6311e02f1be91da982f9b93d3cce99a0","analyzedAt":"2026-08-04T13:40:15.335Z","schemaVersion":2}