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$setwithout 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 useObject.assign(doc, req.body)before calling.save(). Ifreq.bodycarries an_id, Mongoose's internal state gets corrupted.## Fixes### 1. Destructure Out the _id (Best Option)ES6 destructuring makes this a one-liner. Pull_idout 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.

