mongodb/node-mongodb-native · error · MongoInvalidArgumentError

Cursor options must be an object

Error message

Cursor options must be an object

What it means

Thrown by AggregateOperation when options.cursor is defined but is not a plain object. The MongoDB aggregate command expects cursor to be a document (e.g. { batchSize: N }), and the driver validates the shape before sending the wire message.

Solutions

  1. Pass cursor as an object: aggregate(pipeline, { cursor: { batchSize: 100 } }).
  2. If you only need batchSize, prefer the top-level option aggregate(pipeline, { batchSize: 100 }) which the driver wraps for you.
  3. Drop the cursor option entirely if you want server defaults.

Example fix

// before
const cur = collection.aggregate(pipeline, { cursor: 100 });

// after
const cur = collection.aggregate(pipeline, { cursor: { batchSize: 100 } });
// or simply
const cur = collection.aggregate(pipeline, { batchSize: 100 });
Defensive patterns

Strategy: type-guard

Validate before calling

if (options?.cursor != null && (typeof options.cursor !== 'object' || Array.isArray(options.cursor))) {
  throw new TypeError('aggregate cursor option must be a plain object');
}
await collection.aggregate(pipeline, options);

Type guard

const isCursorOptions = (v: unknown): v is { batchSize?: number } =>
  v != null && typeof v === 'object' && !Array.isArray(v);

Prevention

When it happens

Trigger: Passing { cursor: 100 } or { cursor: true } or { cursor: 'batchSize' } to collection.aggregate() or db.aggregate(). Most commonly from mis-typed batchSize helpers that return a primitive.

Common situations: Wrapping batchSize incorrectly: aggregate(pipeline, { cursor: { batchSize } }) is right, but aggregate(pipeline, { cursor: batchSize }) is wrong. Plain-JS code that constructs options dynamically.

Related errors


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

Appendix: source

Thrown at src/operations/aggregate.ts:84

    // determine if we have a write stage, override read preference if so
    this.hasWriteStage = false;
    if (typeof options?.out === 'string') {
      this.pipeline = this.pipeline.concat({ $out: options.out });
      this.hasWriteStage = true;
    } else if (pipeline.length > 0) {
      const finalStage = pipeline[pipeline.length - 1];
      if (finalStage.$out || finalStage.$merge) {
        this.hasWriteStage = true;
      }
    }

    if (!this.hasWriteStage) {
      delete this.options.writeConcern;
    }

    if (options?.cursor != null && typeof options.cursor !== 'object') {
      throw new MongoInvalidArgumentError('Cursor options must be an object');
    }

    this.SERVER_COMMAND_RESPONSE_TYPE = this.explain ? ExplainedCursorResponse : CursorResponse;
  }

  override get commandName() {
    return 'aggregate' as const;
  }

  override get canRetryRead(): boolean {
    return !this.hasWriteStage;
  }

  addToPipeline(stage: Document): void {
    this.pipeline.push(stage);
  }

  override buildCommandDocument(): Document {

View on GitHub (pinned to dce7939f86)