{"id":"3d89ded6b683147c","repo":"mongodb/node-mongodb-native","slug":"unexpected-null-session-a-cursor-creating-command","errorCode":null,"errorMessage":"Unexpected null session. A cursor creating command should have set this","messagePattern":"Unexpected null session\\. A cursor creating command should have set this","errorType":"exception","errorClass":"MongoRuntimeError","httpStatus":null,"severity":"error","filePath":"src/cursor/abstract_cursor.ts","lineNumber":872,"sourceCode":"  protected abstract _initialize(\n    session: ClientSession | undefined\n  ): Promise<InitialCursorResponse>;\n\n  /** @internal */\n  async getMore(): Promise<CursorResponse> {\n    if (this.cursorId == null) {\n      throw new MongoRuntimeError(\n        'Unexpected null cursor id. A cursor creating command should have set this'\n      );\n    }\n    if (this.selectedServer == null) {\n      throw new MongoRuntimeError(\n        'Unexpected null selectedServer. A cursor creating command should have set this'\n      );\n    }\n\n    if (this.cursorSession == null) {\n      throw new MongoRuntimeError(\n        'Unexpected null session. A cursor creating command should have set this'\n      );\n    }\n    const getMoreOptions = {\n      ...this.cursorOptions,\n      session: this.cursorSession,\n      batchSize: this.cursorOptions.batchSize\n    };\n\n    const getMoreOperation = new GetMoreOperation(\n      this.cursorNamespace,\n      this.cursorId,\n      this.selectedServer,\n      getMoreOptions\n    );\n\n    return await executeOperation(this.cursorClient, getMoreOperation, this.timeoutContext);\n  }","sourceCodeStart":854,"sourceCodeEnd":890,"githubUrl":"https://github.com/mongodb/node-mongodb-native/blob/3366c21a6311e02f1be91da982f9b93d3cce99a0/src/cursor/abstract_cursor.ts#L854-L890","documentation":"Thrown by AbstractCursor.getMore() when cursorSession is null at getMore time. The session is created in cursorInit() (abstract_cursor.ts:910) if not already present. Because fetchBatch() initializes the cursor before calling getMore(), a null session here implies cursorInit() was bypassed or the session was cleared (e.g. by a prior cleanup()) without the cursor being marked dead/closed.","triggerScenarios":"Calling the internal getMore() after cleanup() has nulled cursorSession but before isClosed reflects it, or a custom subclass that does not delegate to the base cursorInit(). Concurrent close()+iteration races can also null the session between the guard and the call.","commonSituations":"Concurrent iteration and close() on the same cursor without external synchronization, or test code that manually nulls internal fields.","solutions":["Avoid concurrent iteration and close() on the same cursor; serialize access with an async mutex or stop iteration before closing","Use the public iteration APIs exclusively so cursorInit() always runs first","If subclassing, never null cursorSession yourself; let cleanup() manage it"],"exampleFix":"// before\nconst cursor = coll.find();\nawait Promise.all([cursor.next(), cursor.close()]); // race\n// after\nconst cursor = coll.find();\ntry {\n  for await (const doc of cursor) handle(doc);\n} finally {\n  await cursor.close();\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  for await (const doc of cursor) handle(doc);\n} catch (e) {\n  if (e instanceof MongoRuntimeError && /null session/.test(e.message)) {\n    // cursor was closed concurrently; recreate it\n    cursor = coll.find(filter);\n  } else throw e;\n}","preventionTips":["Never iterate and close() the same cursor concurrently","Serialize cursor access with an async mutex if shared","Let cleanup() manage cursorSession; do not null it manually"],"tags":["cursor","internal","session","concurrency","driver-bug"],"analyzedSha":"3366c21a6311e02f1be91da982f9b93d3cce99a0","analyzedAt":"2026-08-04T13:40:15.335Z","schemaVersion":2}