{"id":"fa114b5f86ed9744","repo":"mongodb/node-mongodb-native","slug":"cursor-options-must-be-an-object","errorCode":null,"errorMessage":"Cursor options must be an object","messagePattern":"Cursor options must be an object","errorType":"exception","errorClass":"MongoInvalidArgumentError","httpStatus":null,"severity":"error","filePath":"src/operations/aggregate.ts","lineNumber":84,"sourceCode":"\n    // determine if we have a write stage, override read preference if so\n    this.hasWriteStage = false;\n    if (typeof options?.out === 'string') {\n      this.pipeline = this.pipeline.concat({ $out: options.out });\n      this.hasWriteStage = true;\n    } else if (pipeline.length > 0) {\n      const finalStage = pipeline[pipeline.length - 1];\n      if (finalStage.$out || finalStage.$merge) {\n        this.hasWriteStage = true;\n      }\n    }\n\n    if (!this.hasWriteStage) {\n      delete this.options.writeConcern;\n    }\n\n    if (options?.cursor != null && typeof options.cursor !== 'object') {\n      throw new MongoInvalidArgumentError('Cursor options must be an object');\n    }\n\n    this.SERVER_COMMAND_RESPONSE_TYPE = this.explain ? ExplainedCursorResponse : CursorResponse;\n  }\n\n  override get commandName() {\n    return 'aggregate' as const;\n  }\n\n  override get canRetryRead(): boolean {\n    return !this.hasWriteStage;\n  }\n\n  addToPipeline(stage: Document): void {\n    this.pipeline.push(stage);\n  }\n\n  override buildCommandDocument(): Document {","sourceCodeStart":66,"sourceCodeEnd":102,"githubUrl":"https://github.com/mongodb/node-mongodb-native/blob/3366c21a6311e02f1be91da982f9b93d3cce99a0/src/operations/aggregate.ts#L66-L102","documentation":"Thrown by the AggregateOperation constructor when options.cursor is defined but is not an object. In the aggregation command, cursor must be a document (e.g. { batchSize: 100 }); passing a boolean or number triggers MongoInvalidArgumentError because the wire protocol expects an object for the cursor field.","triggerScenarios":"Calling collection.aggregate(pipeline, { cursor: true }) or { cursor: 1 } (legacy/loose-typed patterns). Spreading a config object whose cursor field is a non-object. Migrating from an older driver or shell habit.","commonSituations":"Old tutorials using cursor:true; dynamic options objects built from user input; copy-paste from mongo shell syntax.","solutions":["Remove the cursor option entirely (the driver sets a default cursor automatically).","If you need batchSize, pass it as cursor: { batchSize: N } or directly as batchSize: N.","Validate user-supplied options: delete opts.cursor if it is not an object before calling aggregate."],"exampleFix":"// before\ncollection.aggregate(pipeline, { cursor: true });\n\n// after\ncollection.aggregate(pipeline, { cursor: { batchSize: 100 } });","handlingStrategy":"validation","validationCode":"if (opts.cursor != null && typeof opts.cursor !== 'object') delete opts.cursor;","typeGuard":"function isCursorObject(v): boolean { return v == null || (typeof v === 'object' && !Array.isArray(v)); }","tryCatchPattern":null,"preventionTips":["Don't pass cursor:true (driver sets a default cursor)","Pass batchSize directly or as cursor:{batchSize:N}","Sanitize user-supplied option objects before aggregate"],"tags":["aggregation","cursor","options","type-validation"],"analyzedSha":"3366c21a6311e02f1be91da982f9b93d3cce99a0","analyzedAt":"2026-08-04T13:40:15.335Z","schemaVersion":2}