Every Update Has Three Parts
An update call takes a filter saying which documents to change, an update document saying what to change, and an optional options object. Both updateOne and updateMany use exactly this shape, and the only difference between them is how many matching documents get touched.
updateOne modifies the first matching document and then stops. It does not warn you that five hundred others also matched. If your filter is not specific enough — updating by name when two customers share a name, for instance — you will change one person's record and never learn that you meant a different one. Filter by _id or by a field with a unique index whenever you are updating a specific record.
Read the result object rather than assuming success. matchedCount tells you how many documents the filter found; modifiedCount tells you how many actually changed. A result of matched 1, modified 0 is not a failure — it means the document was found and already held the value you were setting. A result of matched 0 means your filter found nothing, which is usually a bug in the filter.
db.users.updateOne(
{ _id: ObjectId("66ab12cd34ef56789012ab34") }, // filter
{ $set: { city: "Mumbai" } }, // update
{ upsert: false } // options
)
// {
// acknowledged: true,
// insertedId: null,
// matchedCount: 1,
// modifiedCount: 1,
// upsertedCount: 0
// }
// matched 1, modified 0 -> found it, value was already "Mumbai"
// matched 0 -> the filter matched nothing; check it - There is no undo. Before running an update on real data, run the same filter through
find()first and look at what comes back. Thirty seconds of checking prevents the kind of mistake that ends with restoring a backup.
$set and the Danger of Leaving It Out
The update document is not the new version of your record. It is a set of instructions, and every instruction is an operator beginning with $. $set means "give these fields these values and leave everything else alone". It also creates the field if it did not exist, which is how you add a new field to an existing document.
Older MongoDB tutorials contain a genuinely destructive pattern. The removed update() method accepted a plain document with no operators, and treated it as a replacement — every field you did not mention was deleted. Countless people wiped a user's email, password hash and order history while trying to change their city.
Modern MongoDB protects you here: pass a plain document to updateOne and the server refuses it with an error about needing atomic operators. But the lesson still stands, because replaceOne does exactly the old behaviour on purpose. Reach for it only when you truly intend to overwrite a whole document — for example when a form submission carries every field. If you only mean to change two fields, always $set.
// Change two fields, keep the rest
db.users.updateOne({ _id: 1 }, { $set: { city: "Mumbai", verified: true } })
// Rejected — no atomic operator
db.users.updateOne({ _id: 1 }, { city: "Mumbai" })
// MongoServerError: Update document requires atomic operators
// replaceOne really does replace: any field not listed here is GONE
db.users.replaceOne(
{ _id: 1 },
{ name: "Ananya Sharma", city: "Mumbai" }
)
// email, age, joined... all removed - The old method names
update(),insert()andremove()have been dropped frommongoshand the current drivers. If a tutorial uses them it predates MongoDB 4.2 and its advice may be out of date in other ways too.
The Operators You Will Use Every Week
Beyond $set, a handful of operators cover almost all real updates. $unset removes a field entirely — note that the value you give it is ignored, so an empty string is the usual convention. $inc adds a number, and a negative number subtracts, which makes it the correct way to decrement stock or add to a view counter. $mul multiplies. $rename renames a field across documents. $min and $max update only if the new value is lower or higher than what is stored, which is handy for tracking a best score. $currentDate stamps the server's current time.
$inc deserves emphasis because it is the fix for a real bug. The obvious way to reduce stock is to read the document, subtract one in JavaScript, and write the value back. If two orders are processed at the same moment, both read 10, both write 9, and you have sold an item you no longer have. $inc performs the arithmetic inside the database as a single atomic step, so two concurrent decrements always leave 8. Never do read-modify-write on a counter.
// Remove a field completely
db.users.updateOne({ _id: 1 }, { $unset: { temporaryToken: "" } })
// Counters, done safely
db.products.updateOne({ sku: "AB-1" }, { $inc: { stock: -1, unitsSold: 1 } })
db.articles.updateOne({ slug: "mongodb-basics" }, { $inc: { views: 1 } })
// Only raise the stored value if the new one is higher
db.players.updateOne({ _id: 7 }, { $max: { highScore: 940 } })
// Rename a field everywhere, and stamp the change
db.users.updateMany({}, {
$rename: { "mobile": "phone" },
$currentDate: { updatedAt: true }
}) - Applying
$incto a field that does not exist creates it, starting from the value you supplied. Applying it to a field holding a string fails with a type error, which is another reason to keep numeric fields numeric.
Updating Inside Arrays
Arrays have their own operators. $push appends a value, and with $each it appends several; adding $slice keeps only the last few entries, which is exactly how you maintain a "recently viewed" list that never grows without limit. $addToSet is like $push but does nothing if the value is already present, so it is the right choice for tags and other sets. $pull removes every element matching a condition, and $pop removes one element from the end (1) or the start (-1).
Changing an element that is already in the array is the harder case. The positional operator $ refers to the first element that your filter matched, so the array field must appear in the filter for it to have a meaning. To update every element, use $[]. To update only the elements meeting a condition, use $[name] together with arrayFilters, which lets you say things like "mark every subject with 40 or more as passed".
// Append, with a cap of the 5 most recent
db.users.updateOne(
{ _id: 1 },
{ $push: { recentlyViewed: { $each: ["sku-88"], $slice: -5 } } }
)
// Sets and removals
db.products.updateOne({ sku: "AB-1" }, { $addToSet: { tags: "sale" } })
db.products.updateOne({ sku: "AB-1" }, { $pull: { tags: "clearance" } })
db.queues.updateOne({ _id: 1 }, { $pop: { jobs: -1 } }) // remove the oldest
// Change one matched element: results.$ is the element the filter found
db.students.updateOne(
{ _id: 1, "results.subject": "Maths" },
{ $set: { "results.$.marks": 95 } }
)
// Change every element that meets a condition
db.students.updateMany(
{},
{ $set: { "results.$[r].passed": true } },
{ arrayFilters: [ { "r.marks": { $gte: 40 } } ] }
) $pushwill happily grow an array for ever. An array of every comment, event or log line inside one document eventually collides with the 16 MB document limit, and long before that it makes the document slow to read. If a list can grow without bound, it belongs in its own collection.
updateMany, Upserts, and findOneAndUpdate
updateMany applies the same change to every matching document — putting a whole category on sale, or backfilling a new field across old records. It is the right tool for migrations, and the wrong tool to run casually, because updateMany({}, ...) touches your entire collection.
An upsert is an update that inserts if nothing matched. Pass { upsert: true } and MongoDB either modifies the matched document or creates one from the filter plus the update. This makes an operation repeatable: a seed script or a webhook handler can run twice without creating duplicates. $setOnInsert supplies fields that should only be written on the creation path — a createdAt timestamp, for example, which must not be overwritten on later runs.
findOneAndUpdate does the update and hands back the document in one round trip. By default it returns the document as it was before the change; pass { returnDocument: "after" } to get the new version. Because the find and the update happen as a single atomic operation, it is the standard way to hand out sequential numbers or to let exactly one worker claim a pending job.
// Bulk change
db.products.updateMany({ category: "Furniture" }, { $set: { onSale: true } })
// Upsert: safe to run again and again
db.products.updateOne(
{ sku: "AB-1" },
{
$set: { name: "Wireless Mouse", price: 799 },
$setOnInsert: { createdAt: new Date() }
},
{ upsert: true }
)
// Atomic counter — returns the value it just produced
db.counters.findOneAndUpdate(
{ _id: "orderNo" },
{ $inc: { seq: 1 } },
{ upsert: true, returnDocument: "after" }
)
// { _id: 'orderNo', seq: 1 } - An upsert whose filter is not backed by a unique index can still create duplicates: two requests arriving at the same instant can both find nothing and both insert. If a field identifies a record uniquely, give it a unique index and let the database enforce it.
