mongodb/node-mongodb-native · error · MongoInvalidArgumentError

Replacement document must not use atomic operators

Error message

Replacement document must not use atomic operators

What it means

Thrown as MongoInvalidArgumentError by FindOperators.replaceOne() (src/bulk/common.ts:749) when the replacement document contains MongoDB atomic operators ($set, $inc, etc.). replaceOne performs a full-document replacement, so the body must be a plain document with the new field values; mixing in $-operators would be rejected by the server and is caught client-side first.

Solutions

  1. Strip the $set wrapper and pass the literal fields: replaceOne({ a: 1, b: 2 }).
  2. If you actually want partial updates, switch the call to updateOne/update (which require the operators).
  3. Audit shared builders so they do not unconditionally prefix field keys with $ operators.

Example fix

// before
bulk.find({ _id: 1 }).replaceOne({ $set: { name: 'Sam' } });

// after
bulk.find({ _id: 1 }).replaceOne({ name: 'Sam' });
Defensive patterns

Strategy: validation

Validate before calling

function isPlainReplacement(doc: unknown): boolean {
  if (doc == null || typeof doc !== 'object' || Array.isArray(doc)) return false;
  return !Object.keys(doc as Record<string, unknown>).some(k => k.startsWith('$'));
}

if (!isPlainReplacement(replacement)) {
  throw new Error('replaceOne body must be a plain document, no $-operators');
}

Type guard

function isReplacementBody(doc: unknown): doc is Record<string, unknown> {
  if (doc == null || typeof doc !== 'object' || Array.isArray(doc)) return false;
  return !Object.keys(doc).some(k => k.startsWith('$'));
}

Try / catch

try {
  bulk.find(filter).replaceOne(replacement);
} catch (e) {
  if (e instanceof MongoInvalidArgumentError && /must not use atomic operators/.test(e.message)) {
    // strip $set wrapper if you accidentally wrapped a replacement
    const inner = (replacement as any).$set ?? replacement;
    bulk.find(filter).replaceOne(inner);
  } else throw e;
}

Prevention

When it happens

Trigger: Calling bulkOp.find(filter).replaceOne({ $set: { a: 1 } }) instead of a plain replacement like replaceOne({ a: 1 }). Guarded by hasAtomicOperators(replacement) at src/bulk/common.ts:748.

Common situations: Developers switch from updateOne to replaceOne without removing the $set wrapper, or share a helper that always wraps fields in $set across both update and replace code paths. Also arises from copy-pasting an update document into a replace call.

Related errors


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

Appendix: source

Thrown at src/bulk/common.ts:749

  }

  /** Add a single update operation to the bulk operation */
  updateOne(updateDocument: Document | Document[]): BulkOperationBase {
    if (!hasAtomicOperators(updateDocument, this.bulkOperation.bsonOptions)) {
      throw new MongoInvalidArgumentError('Update document requires atomic operators');
    }

    const currentOp = buildCurrentOp(this.bulkOperation);
    return this.bulkOperation.addToOperationsList(
      BatchType.UPDATE,
      makeUpdateStatement(currentOp.selector, updateDocument, { ...currentOp, multi: false })
    );
  }

  /** Add a replace one operation to the bulk operation */
  replaceOne(replacement: Document): BulkOperationBase {
    if (hasAtomicOperators(replacement)) {
      throw new MongoInvalidArgumentError('Replacement document must not use atomic operators');
    }

    const currentOp = buildCurrentOp(this.bulkOperation);
    return this.bulkOperation.addToOperationsList(
      BatchType.UPDATE,
      makeUpdateStatement(currentOp.selector, replacement, { ...currentOp, multi: false })
    );
  }

  /** Add a delete one operation to the bulk operation */
  deleteOne(): BulkOperationBase {
    const currentOp = buildCurrentOp(this.bulkOperation);
    return this.bulkOperation.addToOperationsList(
      BatchType.DELETE,
      makeDeleteStatement(currentOp.selector, { ...currentOp, limit: 1 })
    );
  }

View on GitHub (pinned to dce7939f86)