Quick answer
- A write tried to duplicate a value in a unique index. The message names the culprit:
index: email_1 dup key: { email: "a@b.com" }. - Expected duplicate (re-run, import)? Use an upsert instead of insert.
- Real client conflict (signup with an existing email)? Catch code 11000 and return
409. dup key: { field: null }? Two docs are missing the field — use a partial index, not a plain unique one.
The exact error string
MongoServerError: E11000 duplicate key error collection: appdb.users
index: email_1 dup key: { email: "ada@example.com" }
You'll also see these variants — the driver class name changes over versions, and the dup key content tells you which case you're in:
// older driver wording
WriteError: E11000 duplicate key error index: appdb.users.$email_1 dup key: {...}
BulkWriteError: E11000 duplicate key error collection: ... // insertMany
// the "missing field" trap — both documents indexed null
E11000 duplicate key error collection: appdb.users index: email_1 dup key: { email: null }
// a compound unique index — more than one field must be unique together
E11000 duplicate key error collection: appdb.members
index: org_1_email_1 dup key: { org: "acme", email: "ada@example.com" }
MongoDB enforces a unique index by rejecting any insert or update that would put a second document with the same indexed value into the collection. The error hands you everything you need: the index: name (which fields it covers) and the dup key: (the exact value that already exists). Read those two parts first — every fix below flows from what they say.
Fastest fix
If a duplicate is expected — you re-ran a seed script, replayed a message, or want “create or update” — stop using insertOne/create and switch to an upsert. It inserts when the document is new and updates when it exists, atomically, so the duplicate never becomes an error:
db.users.updateOne(
{ email: "ada@example.com" }, // the unique key
{ $set: { name: "Ada" } },
{ upsert: true }
)
30-second triage
Which case are you in? The dup key value tells you:
| What you see | Case | Go to |
|---|---|---|
| A real value that already exists, and a duplicate is fine | Re-run / import / “create or update” | Fix 1 (upsert) |
| A real value, and the duplicate is a user mistake | Client conflict (e.g. email taken) | Fix 2 (catch 11000 → 409) |
{ field: null } | Multiple docs missing the field | Fix 3 (partial index) |
An index: name you didn't expect | A stale or unintended unique index | Fix 4 (inspect & drop) |
index: a_1_b_1 (multiple fields) | A compound unique constraint | Fix 5 (compound) |
Fix 1: upsert instead of insert
An upsert matches on the unique key and either inserts or updates — atomic and race-safe, unlike a find-then-insert which can still collide between the two calls. In Mongoose, findOneAndUpdate returns the resulting document:
// Mongoose — insert-or-update and get the document back
const user = await User.findOneAndUpdate(
{ email },
{ $set: { name } },
{ upsert: true, new: true, setDefaultsOnInsert: true }
);
// use $setOnInsert for fields that should only be written on the initial insert
await User.updateOne(
{ email },
{ $set: { name }, $setOnInsert: { createdAt: new Date() } },
{ upsert: true }
);
Note that a unique index is what makes an upsert safe under concurrency: if two upserts race, one wins the insert and the other retries as an update rather than creating a duplicate.
Fix 2: catch code 11000 and return 409
When the duplicate is a genuine client error — someone registering with an email that's already taken — catch the write error and turn it into a clean response instead of a 500. The modern driver exposes keyValue and keyPattern so you can name the field precisely:
try {
await User.create({ email, name });
} catch (err) {
if (err.code === 11000) {
const field = Object.keys(err.keyValue)[0]; // "email"
return res.status(409).json({ error: `${field} already in use` });
}
throw err; // rethrow anything else
}
Returning 409 Conflict is the correct HTTP semantics here — the request was valid but conflicts with existing state. This mirrors the pattern for the PostgreSQL duplicate key value violates unique constraint error, where you catch SQLSTATE 23505 for the same reason.
Fix 3: the { field: null } trap — use a partial index
This is the most confusing case. A plain unique index treats a missing field as null, and null collides with null — so as soon as a second document omits the field, you get E11000 on { field: null } even though neither document set it. The fix is to make the uniqueness apply only to documents that actually have the field, with a partial index:
// only enforce uniqueness on documents that HAVE a non-null email
db.users.createIndex(
{ email: 1 },
{ unique: true, partialFilterExpression: { email: { $exists: true } } }
)
// Mongoose schema equivalent
new Schema({
email: {
type: String,
index: { unique: true, partialFilterExpression: { email: { $exists: true } } }
}
});
A sparse: true index also skips documents missing the field and is the older tool for this, but it has real limitations — especially in compound indexes, where a document is only skipped if it's missing every indexed field. Prefer a partial index for new work; it's more explicit and more flexible.
Fix 4: inspect and drop a stale index
If the index: name is one you didn't expect, an old unique index is still in place. Editing unique: true in your schema does not drop an index MongoDB already built — you have to remove it yourself. List what actually exists, then drop the offender:
db.users.getIndexes()
// [ { v: 2, key: { _id: 1 }, name: "_id_" },
// { v: 2, key: { email: 1 }, name: "email_1", unique: true } ]
db.users.dropIndex("email_1")
// { nIndexesWas: 2, ok: 1 }
// then recreate it the way you actually want (e.g. as a partial index)
Also watch the reverse problem: a unique index fails to build if the collection already contains duplicates, so the constraint you think is protecting you may never have been created. Clean up existing duplicates first, then create the index. And keep in mind that Mongoose only auto-builds indexes when autoIndex is on (it's on in development, and you usually turn it off in production and manage indexes explicitly).
Fix 5: a compound unique index
When the index name lists more than one field (org_1_email_1), uniqueness is enforced across the combination, not each field alone. The dup key shows the full tuple that collided:
// unique per (org, email): the same email is allowed in different orgs
db.members.createIndex({ org: 1, email: 1 }, { unique: true })
// dup key: { org: "acme", email: "ada@example.com" } -> that exact PAIR exists
// { org: "beta", email: "ada@example.com" } is still allowed
So a compound-index E11000 means the whole tuple was already present. If you actually wanted the email unique globally, you built the wrong index — drop it (Fix 4) and create a single-field unique index instead.
Debugging checklist
- ✓ Read the
index:name anddup key:— they name the field(s) and the exact colliding value - ✓ Expected duplicate? Upsert with
{ upsert: true }instead of insert (Fix 1) - ✓ Real client conflict? Catch
err.code === 11000, useerr.keyValue, return409(Fix 2) - ✓
dup key: { field: null }? Multiple docs miss the field — use a partial index (Fix 3) - ✓ Unexpected index name?
getIndexes(), thendropIndex()the stale one (Fix 4) - ✓ Compound index name (
a_1_b_1)? The whole tuple must be unique, not each field (Fix 5) - ✓ Index never enforced? It may have failed to build over existing duplicates — clean up first
Frequently Asked Questions
What does E11000 duplicate key error mean in MongoDB?
An insert or update tried to store a value that already exists in a unique index. The message names the index and the conflicting value: index: email_1 dup key: { email: "a@b.com" }. MongoDB rejects the write to keep the indexed field unique. Read the index name and dup key to see exactly what collided.
Why do I get E11000 with dup key { field: null }?
A unique index treats a missing field as null, so two documents that both omit the field both index as null and the second insert collides. Either always set the field, or make the index only apply to documents that have it using a partial index: partialFilterExpression: { email: { $exists: true } }. A sparse index also works but is more limited, especially in compound indexes.
How do I insert without failing if the document already exists?
Use an upsert: updateOne(filter, { $set: {...} }, { upsert: true }) inserts when nothing matches the filter and updates otherwise. It is atomic, so it avoids the race condition of checking for the document first and then inserting. In Mongoose, findOneAndUpdate(filter, update, { upsert: true, new: true }) does the same and returns the resulting document.
How do I catch the duplicate key error in Node.js?
Check the error code: if (err.code === 11000). The modern driver and Mongoose also expose err.keyValue ({ email: 'a@b.com' }) and err.keyPattern ({ email: 1 }), so you can build a clean message and return HTTP 409 Conflict instead of a 500. Catching it turns a real client conflict, like signing up with an existing email, into a proper validation response.
Does Mongoose 'unique: true' validate uniqueness?
No. unique: true is not a validator — it tells Mongoose to build a unique index. Uniqueness is enforced by MongoDB at write time as E11000, not by Mongoose validation, so it does not produce a ValidationError and does not run in a normal validate() call. Handle the E11000 (code 11000) in your catch block, and be aware the index only exists once it has actually been built.
Why does E11000 still happen after I removed the unique field?
The index still exists even if your schema no longer declares it. Changing unique: true in code does not drop an index MongoDB already created. List indexes with db.collection.getIndexes() and drop the stale one with db.collection.dropIndex('email_1'). Then recreate it the way you actually want, for example as a partial index.
More database & backend errors
Browse the full reference for MongoDB, PostgreSQL, Node.js, and Docker errors — exact message, cause, and fix.