Type something to search...
MongoDB Error Handling: Common Error Codes and How to Fix Them

MongoDB Error Handling: Common Error Codes and How to Fix Them

MongoDB errors have a habit of showing up at the worst possible moment, with messages that make sense only if you already know what they mean. E11000 duplicate key error collection is clear enough, but "Server selection timed out after 30000 ms" sends plenty of people hunting through server logs for a problem that lives in their connection string. And WriteConflict looks alarming until you realize the driver was designed to retry it for you.

The key to handling MongoDB errors well is knowing which ones are bugs to fix, which are expected conditions your code should handle, and which are transient failures that retrying will solve. Every server error carries a numeric error code and often a set of error labels, and those are what your code should branch on, not the message text.

This guide covers how MongoDB errors are structured in the drivers, the error codes you'll hit most often (with causes and fixes), connection and timeout errors that come from the driver rather than the server, and patterns for handling errors cleanly in Node.js and Python.

How MongoDB Errors Are Structured

In the Node.js driver, all errors extend MongoError. The important subclasses are:

  • MongoServerError: the server rejected the operation. Has a numeric code, usually a codeName, and sometimes extra fields like keyValue or errInfo.
  • MongoBulkWriteError: one or more operations in a bulk write or insertMany failed. Contains writeErrors and the partial result.
  • MongoServerSelectionError: the driver couldn't find a suitable server to talk to. This is a client-side timeout, not a server error.
  • MongoNetworkError and MongoNetworkTimeoutError: the connection broke or a socket operation timed out.

Here's what a server error looks like when you log it:

try {
  await db.collection("users").insertOne({ email: "ada@example.com" });
} catch (err) {
  console.log(err.name, err.code);
  console.log(err.keyValue);
}
MongoServerError 11000
{ email: 'ada@example.com' }

Most server errors also include a human-readable codeName such as "DocumentValidationFailure", which is handy in logs, but the numeric code is what you should compare against.

In PyMongo, the equivalent hierarchy lives in pymongo.errors: OperationFailure (with .code and .details), DuplicateKeyError, WriteError, BulkWriteError, ServerSelectionTimeoutError, NetworkTimeout, and AutoReconnect.

Error Labels

Some errors carry labels, which describe how the error should be handled rather than what went wrong. The two you'll see most:

  • RetryableWriteError: the write can safely be retried (and the driver already retries once if retryable writes are enabled).
  • TransientTransactionError: the whole transaction can be retried from the start.

Check them with err.hasErrorLabel("TransientTransactionError") in Node.js or exc.has_error_label(...) in PyMongo.

The Error Codes You'll Actually Hit

This table is the quick reference. Details for each follow.

CodeNameTypical causeRetry?
11000DuplicateKeyUnique index violationNo, handle it
121DocumentValidationFailure$jsonSchema validator rejected the writeNo, fix the data
2BadValueInvalid argument or operator usageNo, fix the code
9FailedToParseMalformed query or updateNo, fix the code
13UnauthorizedUser lacks the required roleNo, fix permissions
18AuthenticationFailedWrong credentials or auth databaseNo, fix config
50MaxTimeMSExpiredOperation exceeded maxTimeMSMaybe, after tuning
112WriteConflictConcurrent writes to the same documentYes, usually automatic
251NoSuchTransactionTransaction aborted or expiredYes, whole transaction
10334BSONObjectTooLargeDocument over 16 MBNo, redesign
292QueryExceededMemoryLimitNoDiskUseAllowedSort or group exceeded memory with disk use disabledNo, add index or allow disk use
40ConflictingUpdateOperatorsTwo operators modify the same pathNo, fix the update
66ImmutableFieldTried to change _idNo, fix the update
26NamespaceNotFoundCollection or database doesn't exist for this commandDepends

A note on code 16500: if you see it, you're almost certainly talking to Azure Cosmos DB's MongoDB API, which uses that code for request rate throttling. It isn't a MongoDB server error, and the fix is on the Cosmos side.

11000: Duplicate Key

The most common error in any app with unique indexes.

E11000 duplicate key error collection: shop.users index: email_1 dup key: { email: "ada@example.com" }

This is usually an expected condition, not a bug. A user tried to sign up with an email that's already registered. Catch it and return a meaningful response:

try {
  await users.insertOne({ email, name });
} catch (err) {
  if (err.code === 11000) {
    const field = Object.keys(err.keyValue ?? {})[0] ?? "field";
    return res.status(409).json({ error: `That ${field} is already in use` });
  }
  throw err;
}

Don't pre-check with findOne and then insert. Two concurrent requests can both pass the check. Let the unique index enforce uniqueness and handle the error. If the error surprises you, it's often because an upsert filter didn't match the unique field exactly, or a case difference means two "identical" values aren't identical. We cover this in depth in handling duplicate key errors and unique constraints.

121: Document Validation Failure

Your collection has a validator, and the document didn't pass it. The error's errInfo tells you exactly why:

try {
  await db.collection("orders").insertOne({ total: "twelve" });
} catch (err) {
  if (err.code === 121) {
    console.dir(err.errInfo.details, { depth: null });
  }
}
{
  "operatorName": "$jsonSchema",
  "schemaRulesNotSatisfied": [
    {
      "operatorName": "required",
      "specifiedAs": { "required": ["customerId", "total"] },
      "missingProperties": ["customerId"]
    },
    {
      "operatorName": "properties",
      "propertiesNotSatisfied": [
        {
          "propertyName": "total",
          "details": [
            {
              "operatorName": "bsonType",
              "specifiedAs": { "bsonType": "decimal" },
              "reason": "type did not match",
              "consideredValue": "twelve",
              "consideredType": "string"
            }
          ]
        }
      ]
    }
  ]
}

Log errInfo.details in full during development. It points you straight to the failing field. A frequent surprise is numeric types: a validator requiring bsonType: "int" rejects a JavaScript number with a fractional part, and a validator requiring "decimal" rejects any plain number.

2 and 9: BadValue and FailedToParse

These mean the server couldn't make sense of your command. Common triggers:

  • Using an update operator in a replacement, or a field in an update without an operator (the driver catches the second one with a client-side error before it reaches the server).
  • $regex with an invalid pattern.
  • An unknown operator such as $contains (which doesn't exist).
  • Projection mixing inclusion and exclusion, like { name: 1, email: 0 } (the server reports this as a specific projection error code, but it's the same category of bug).

These are always bugs. The fix is in the code, and the error message usually names the offending operator or field.

13 and 18: Unauthorized and AuthenticationFailed

18 means the credentials didn't work. Check the username and password (URL-encode special characters in the connection string), and make sure authSource points to the database where the user was created. Users created in admin need authSource=admin if your connection string names a different database.

mongodb://appuser:p%40ssw0rd@db.example.com:27017/shop?authSource=admin

13 means you authenticated, but the user doesn't have permission for this action on this resource. A user with read on shop trying to insert, or an app user trying to run createIndex without the createIndex privilege. Grant the specific role needed, and resist the temptation to hand out root.

50: MaxTimeMSExpired

The operation ran longer than the maxTimeMS limit you set:

await orders.find({ status: "pending" }, { maxTimeMS: 2000 }).toArray();

This error is doing its job: protecting your database from runaway queries. Treat it as a performance signal. Run the query with explain("executionStats") and look for a COLLSCAN or a huge totalDocsExamined relative to nReturned. Usually an index fixes it. See using explain to analyze slow queries.

Newer drivers also support client-side operation timeouts through the timeoutMS option (Client-Side Operation Timeout, or CSOT). When that timeout fires, you get a driver-side timeout error rather than code 50.

112: WriteConflict

Two operations tried to modify the same document at the same time, and one lost. Outside transactions, the server retries write conflicts internally and you rarely see them. Inside a transaction, a write conflict aborts the transaction and comes back with the TransientTransactionError label.

The fix is to retry the whole transaction, which is exactly what the driver's withTransaction helper does:

const session = client.startSession();
try {
  await session.withTransaction(async () => {
    await accounts.updateOne(
      { _id: from },
      { $inc: { balance: -amount } },
      { session },
    );
    await accounts.updateOne(
      { _id: to },
      { $inc: { balance: amount } },
      { session },
    );
  });
} finally {
  await session.endSession();
}

If you see frequent write conflicts, you have a hot document that many transactions touch. Consider restructuring to reduce contention, for example by sharding a counter across several documents.

251: NoSuchTransaction

The transaction you're trying to use no longer exists on the server. Causes include an earlier error that aborted it, the transaction exceeding transactionLifetimeLimitSeconds (60 seconds by default), or a failover. It usually carries TransientTransactionError, and the fix is to retry the whole transaction. If it happens because transactions run longer than 60 seconds, the transaction is too big: break the work into smaller units.

10334: BSONObjectTooLarge

A document exceeded the 16 MB BSON limit. Depending on where the check happens, you may instead get a client-side error from the driver before the document is even sent, but the cause is the same. This almost always means an unbounded array: comments embedded in a post, events appended to a user, log lines pushed into a single document. Increasing the limit isn't possible. Move the growing data into its own collection with a reference back to the parent, or use the bucket pattern for time-based data. For large binary files, use GridFS.

292: Memory Limit Exceeded

Blocking stages like $sort and $group have a per-stage memory limit (100 MB). Since MongoDB 6.0, allowDiskUseByDefault is on, so most queries spill to disk automatically. You'll see this error when disk use is disabled for the query or cluster:

Sort exceeded memory limit of 104857600 bytes, but did not opt in to external sorting.

The better fix is an index that supports the sort so no in-memory sort is needed. Otherwise, allow disk use explicitly:

await orders.aggregate(pipeline, { allowDiskUse: true }).toArray();

40 and 66: Update Operator Mistakes

40 (ConflictingUpdateOperators) happens when two operators in one update touch the same field, like $set and $inc both on stats.views, or $set on address and $set on address.city together. Combine them into one operation per path.

66 (ImmutableField) happens when an update or replacement tries to change _id. It often shows up with replaceOne or upserts that pass a whole object including a new _id. Strip _id from the replacement or make sure it matches.

Errors That Don't Come From the Server

Server Selection Timeout

MongoServerSelectionError: Server selection timed out after 30000 ms

The driver tried for 30 seconds (the serverSelectionTimeoutMS default) to find a server matching your read preference and couldn't. The server never saw your query. Common causes, roughly in order of frequency:

  1. Network access. On Atlas, your IP isn't in the access list. On a VPC, a security group or firewall is blocking port 27017.
  2. Wrong host or DNS. A typo in the hostname, or a mongodb+srv:// URI in an environment where SRV lookups fail.
  3. Replica set name mismatch. The replicaSet in your URI doesn't match the actual set, or the hosts advertise internal hostnames your client can't resolve (common with Docker).
  4. TLS problems. Connecting without TLS to a server that requires it, or a certificate the client doesn't trust.
  5. No eligible server. The read preference is primary and there's no primary (for example, during an election or when a majority of nodes is down).

A quick way to isolate it: try connecting with mongosh using the same URI from the same machine. If mongosh also fails, it's network or configuration, not your code.

Network Errors

MongoNetworkError and PyMongo's AutoReconnect mean an established connection broke. Brief network blips, failovers, and idle connections closed by a load balancer or NAT gateway all cause this. Modern drivers retry most reads and writes once automatically (retryable reads and writes, both enabled by default), so if you're still seeing these errors, the problem lasted longer than a single retry could cover. See retryable writes and reads for how that works.

Handling Errors in Bulk Operations

insertMany and bulkWrite can partially succeed. With ordered: true (the default), processing stops at the first error. With ordered: false, everything that can succeed does, and all failures are reported together:

try {
  await products.insertMany(docs, { ordered: false });
} catch (err) {
  if (err.name === "MongoBulkWriteError") {
    const dupes = err.writeErrors.filter((e) => e.code === 11000).length;
    console.log(
      `Inserted ${err.result.insertedCount}, skipped ${dupes} duplicates`,
    );
    const other = err.writeErrors.filter((e) => e.code !== 11000);
    if (other.length) throw err;
  } else {
    throw err;
  }
}

This is the standard pattern for idempotent imports: insert everything unordered and ignore duplicate key errors.

A Central Error Mapper

Rather than sprinkling code checks everywhere, map MongoDB errors to your application's error types in one place:

import {
  MongoServerError,
  MongoServerSelectionError,
  MongoNetworkError,
} from "mongodb";

export function toAppError(err) {
  if (err instanceof MongoServerError) {
    switch (err.code) {
      case 11000:
        return {
          status: 409,
          message: "Resource already exists",
          fields: err.keyValue,
        };
      case 121:
        return {
          status: 422,
          message: "Invalid data",
          details: err.errInfo?.details,
        };
      case 50:
        return { status: 503, message: "Request took too long, try again" };
      case 13:
        return { status: 500, message: "Server misconfiguration" };
    }
  }
  if (
    err instanceof MongoServerSelectionError ||
    err instanceof MongoNetworkError
  ) {
    return { status: 503, message: "Database temporarily unavailable" };
  }
  return { status: 500, message: "Unexpected error" };
}

Notice that 13 maps to a 500, not a 403. The end user isn't unauthorized; your application's database user is, which is a bug for you to fix. Never return raw MongoDB error messages to clients. They can reveal collection names, index names, and data values.

The Python equivalent with PyMongo:

from pymongo.errors import DuplicateKeyError, OperationFailure, ServerSelectionTimeoutError

def to_app_error(exc: Exception) -> tuple[int, str]:
    if isinstance(exc, DuplicateKeyError):
        return 409, "Resource already exists"
    if isinstance(exc, OperationFailure):
        if exc.code == 121:
            return 422, "Invalid data"
        if exc.code == 50:
            return 503, "Request took too long, try again"
    if isinstance(exc, ServerSelectionTimeoutError):
        return 503, "Database temporarily unavailable"
    return 500, "Unexpected error"

DuplicateKeyError is a subclass of OperationFailure, which is why it's checked first.

Best Practices

Branch on codes and labels, never on messages. Message text changes between server versions. Codes and labels are stable.

Let indexes and validators enforce rules. Catching 11000 and 121 is more reliable than pre-checking in application code, which has race conditions.

Use withTransaction instead of hand-rolled transaction loops. It retries TransientTransactionError and handles UnknownTransactionCommitResult correctly.

Set time limits. maxTimeMS (or timeoutMS) on user-facing queries turns a slow query into a fast, handled error rather than a hung request.

Log the full error server-side. code, codeName, errorLabels, keyValue, and errInfo are invaluable for debugging. Return only a safe summary to clients.

Conclusion

Most MongoDB errors fall into three groups: expected conditions to handle (duplicate keys, validation failures), bugs to fix (bad values, conflicting operators, permission problems, oversized documents), and transient failures to retry (write conflicts, transaction aborts, network blips). Once you know which group an error belongs to, the right response is usually obvious, and the error code tells you the group.

Search your codebase for places that catch MongoDB errors by matching message strings like "duplicate key". Replace each one with a check on err.code, and route them all through a single error mapper so your API responses stay consistent.

Tags :
Share :

Related Posts

A Complete Guide to MongoDB Query Operators

A Complete Guide to MongoDB Query Operators

Your first MongoDB queries are usually simple equality filters: find the user with this email, find orders with this status. That covers a surprising

Continue Reading
Async MongoDB in Python with Motor and FastAPI

Async MongoDB in Python with Motor and FastAPI

FastAPI runs your endpoints on an event loop. That's what lets a single worker juggle hundreds of concurrent requests: while one request waits on the

Continue Reading
Atlas Online Archive: Tiering Cold Data to Cut Costs

Atlas Online Archive: Tiering Cold Data to Cut Costs

Look at almost any production database and you'll find the same shape. A small slice of recent data gets nearly all the reads and writes: this week's

Continue Reading