Fixing MongoDB Update Error: After applying the update, the (immutable) field '_id' was found to have been altered

intermediate๐Ÿƒ MongoDB2026-05-06| MongoDB 4.0+, Node.js/Python/Go Drivers, Mongoose ODM, Linux/Docker environments

Error Message

After applying the update, the (immutable) field '_id' was found to have been altered
#mongodb#update#immutable#_id

The ProblemI was building an update endpoint for a Node.js API when MongoDB suddenly started rejecting every single write. The payload looked fine. The query looked fine. But the error was relentless:

WriteError: After applying the update, the (immutable) field '_id' was found to have been altered

In MongoDB, _id is the permanent, unique identifier for a document. You can never change it after creation โ€” not even to the same value. The frustrating part? You probably didn't mean to touch it at all. MongoDB sees the field in your $set payload and refuses the entire operation.

How This Usually HappensNine times out of ten, this isn't intentional. It sneaks in through one of these patterns:

  • Spreading req.body directly: Your client sends a full object โ€” including _id โ€” and you pass it straight into $set without filtering.- Generic update helpers: A shared utility function accepts a full document and writes it back to MongoDB without stripping metadata fields.- Mongoose + Object.assign(): You fetch a document, then use Object.assign(doc, req.body) before calling .save(). If req.body carries an _id, Mongoose's internal state gets corrupted.## Fixes### 1. Destructure Out the _id (Best Option)ES6 destructuring makes this a one-liner. Pull _id out on its own and let the rest go into your update payload:
// Broken โ€” passes _id into $set
const updateData = req.body;
await db.collection('users').updateOne(
  { _id: userId },
  { $set: updateData }
);

// Fixed โ€” _id never reaches $set
const { _id, ...payload } = req.body;
await db.collection('users').updateOne(
  { _id: userId },
  { $set: payload }
);

This is clean, readable, and works in any Node.js version from ES2018 onward.

2. Delete the Key ManuallyIf destructuring doesn't fit โ€” say you're in a middleware that mutates an existing object โ€” just delete the key directly:

const updateData = { ...req.body };
delete updateData._id;

db.collection('products').updateOne({ _id: productId }, { $set: updateData });

Make sure you spread into a new object first ({ ...req.body }). Mutating req.body itself is a bad habit that causes subtle bugs elsewhere in the request lifecycle.

3. Mongoose: Use findByIdAndUpdate Instead of save()With Mongoose, the safest path is to skip Object.assign() entirely:

// Risky โ€” req.body._id can corrupt the document's internal tracker
const user = await User.findById(id);
Object.assign(user, req.body);
await user.save();

// Safe โ€” Mongoose handles _id correctly here
const { _id, ...updateFields } = req.body;
await User.findByIdAndUpdate(id, updateFields, { new: true });

The { new: true } option returns the updated document instead of the original โ€” handy when you need to send the result back to the client immediately.

How to Confirm the Fix Worked- Check logs: The WriteError should be gone. If you're still seeing it, there's another code path passing _id through.- Log the payload: Drop a console.log(payload) right before the database call and confirm _id is absent from the object.- Inspect the document: Open MongoDB Compass or Atlas and verify the document updated correctly โ€” values changed, _id unchanged.## Preventing This From Coming Back- Allowlist your fields: Instead of blocking _id, explicitly pick which fields you accept โ€” for example, const { name, email, role } = req.body. This kills the Mass Assignment vulnerability at the same time.- Lodash users: _.omit(data, ['_id', '__v']) strips both the Mongo ID and Mongoose's version key in one call.- API design habit: Keep _id in the URL path (PUT /api/users/:id), never in the request body. Your frontend shouldn't be sending it, and your backend shouldn't be accepting it.

Related Error Notes