mongodb/node-mongodb-native · error · MongoInvalidArgumentError

Could not serialize operation to BSON

Error message

Could not serialize operation to BSON

What it means

Thrown when BSON.serialize fails on a single client-side bulk write operation. The driver wraps the underlying BSON error (cause) so you can see exactly why serialization aborted. Common BSON-serialization failures include values that BSON cannot represent: undefined nested in documents (with the driver's checkKeys behavior), functions, Symbols, circular references, or oversized keys.

Solutions

  1. Inspect error.cause — it carries the exact BSON error message and path.
  2. Strip non-serializable fields (functions, symbols, undefined) before building the model; convert class instances to plain objects.
  3. If circular references are involved, serialize with a replacer or use EJSON for the problematic values.
  4. For BigInt values, wrap with bson.Long or bson.Int32 as appropriate.

Example fix

// before
await client.bulkWrite([{
  insertOne: { namespace: 'db.coll', document: { req, handler } } // req is a circular http request object
}]);

// after
await client.bulkWrite([{
  insertOne: { namespace: 'db.coll', document: { id: req.id, url: req.url } }
}]);
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-validate with the same BSON the driver uses
import { serialize } from 'bson';
try {
  serialize(opDocument, { checkKeys: false });
} catch (e) {
  throw new Error(`Document will not serialize: ${e.message}`);
}

Type guard

const isPlainSerializable = (v: unknown): boolean => {
  if (v == null || typeof v !== 'object') return typeof v !== 'function' && typeof v !== 'symbol';
  return Object.values(v).every(isPlainSerializable);
};

Try / catch

try {
  await client.bulkWrite(models);
} catch (e) {
  if (/Could not serialize operation to BSON/.test(e.message)) {
    // inspect e.cause for the exact field, fix, and retry that batch
  }
}

Prevention

When it happens

Trigger: Passing a model whose filter/update/document contains a function, Symbol, circular reference, or a key containing '.' or starting with '$' where disallowed. Also a 32-bit-int overflow or a BigInt outside the supported range.

Common situations: Storing class instances with method references; serializing request objects from HTTP libraries that contain circular refs; accidentally including a Mongoose document with non-plain-object internals; passing Decimal128/Long constructed incorrectly.

Related errors


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

Appendix: source

Thrown at src/operations/client_bulk_write/command_builder.ts:136

    while (this.currentModelIndex < this.models.length) {
      const model = this.models[this.currentModelIndex];
      const ns = model.namespace;
      const nsIndex = namespaces.get(ns);

      // Multi updates are not retryable.
      if (model.name === 'deleteMany' || model.name === 'updateMany') {
        this.isBatchRetryable = false;
      }

      if (nsIndex != null) {
        // Build the operation and serialize it to get the bytes buffer.
        const operation = buildOperation(model, nsIndex, this.pkFactory, this.options);
        let operationBuffer;
        try {
          operationBuffer = BSON.serialize(operation);
        } catch (cause) {
          throw new MongoInvalidArgumentError(`Could not serialize operation to BSON`, { cause });
        }

        validateBufferSize('ops', operationBuffer, maxBsonObjectSize);

        // Check if the operation buffer can fit in the command. If it can,
        // then add the operation to the document sequence and increment the
        // current length as long as the ops don't exceed the maxWriteBatchSize.
        if (
          commandLength + operationBuffer.length < maxMessageSizeBytes &&
          command.ops.documents.length < maxWriteBatchSize
        ) {
          // Pushing to the ops document sequence returns the total byte length of the document sequence.
          commandLength = MESSAGE_OVERHEAD_BYTES + command.ops.push(operation, operationBuffer);
          // Increment the builder's current model index.
          this.currentModelIndex++;
        } else {
          // The operation cannot fit in the current command and will need to
          // go in the next batch. Exit the loop.

View on GitHub (pinned to dce7939f86)