Update Operators

An update takes a filter and an update document whose top-level keys are all operators. A plain document — updateOne({...}, { copies: 4 }) — is rejected with "Update document requires atomic operators", the server protecting you: a bare document would mean replace, and replacing when you meant to patch is how fields disappear (Replace and Upsert).

The field operators are few enough to learn at once: $set writes a field and $unset removes it; $inc and $mul change a number in place; $min and $max write only if the new value is smaller or larger; $rename moves a field, keeping its value; $currentDate stamps the server clock; and $setOnInsert applies only when an upsert inserts (Replace and Upsert).

Four updates, and the two results that mean nothing happenedJavaScript
const show = (label, r) =>
  console.log(label, "matched", r.matchedCount, "modified", r.modifiedCount);
show("one book :", db.books.updateOne(
  { title: "Refactoring" },
  { $set: { publisher: "Addison-Wesley" }, $inc: { copies: 2 },
    $currentDate: { updatedAt: true }, $unset: { tags: "" } }));
show("older two:", db.books.updateMany({ year: { $lt: 2018 } }, { $mul: { copies: 2 } }));
show("no match :", db.books.updateOne({ title: "Nonexistent" }, { $set: { copies: 9 } }));
show("no change:", db.books.updateOne({ title: "Refactoring" }, { $set: { copies: 4 } }));
console.log(db.books.findOne({ title: "Refactoring" }, { _id: 0, copies: 1, publisher: 1 }));
Output
one book : matched 1 modified 1
older two: matched 2 modified 2
no match : matched 0 modified 0
no change: matched 1 modified 0
{ copies: 4, publisher: 'Addison-Wesley' }

Operators combine in one update document and all apply atomically: the first call sets a field, increments a number, stamps a date and drops the tags array in one pass. They may not touch the same path twice — { $set: { copies: 4 }, $inc: { copies: 1 } } is a conflict error. $inc is also the only correct way to change a counter: reading copies, adding one in JavaScript and writing it back loses increments under concurrency, while $inc runs under the document lock, so two simultaneous calls both count.

Now read the last two results, two failures wearing the same face. matched 0 means the filter found nothing: a typo, a string "2018" where the data holds the number 2018, or an _id passed as a string instead of an ObjectId. matched 1 modified 0 means the filter worked and the document already held the value you wrote, so the server skipped the write. An API returning 404 on modifiedCount === 0 rejects a valid idempotent PUT: branch on matchedCount for "does it exist", on modifiedCount for "did anything change".