Why findoneandupdate mongoose Returns the Old Document (And How to Fix It)
Mongoose's findOneAndUpdate returns the pre-update document by default because it wraps MongoDB's findAndModify command, which uses new: false unless you explicitly set new: true or the modern equivalent returnDocument: 'after'.
The findoneandupdate mongoose method is a staple for atomic update operations, yet developers frequently encounter confusion when the returned document reflects the old state rather than the updated one. This behavior stems from the underlying MongoDB server implementation in the mongodb/mongo repository rather than a bug in Mongoose itself. Understanding how the server processes the new flag reveals why you must explicitly request the post-update document.
The Root Cause: MongoDB's findAndModify Command
At the database level, Mongoose's findOneAndUpdate invokes MongoDB's findAndModify command. This command accepts a new boolean option that determines which version of the document the server returns:
new: false(default): Returns the document as it was before the update.new: true: Returns the document after the update has been applied.
When you omit this option, the server defaults to false, sending back the original document even though the update succeeds.
Server-Side Logic in find_and_modify.cpp
The MongoDB server constructs the response in src/mongo/db/commands/query_cmd/find_and_modify.cpp. The CmdFindAndModify::Invocation::typedRun method calls buildResponse to generate the reply:
write_ops::FindAndModifyCommandReply CmdFindAndModify::Invocation::typedRun(
...
return buildResponse(updateResult, req.getRemove().value_or(false), docFound);
The buildResponse function uses the new option parsed from the request to decide whether to populate the response with the pre-update or post-update document.
How the new Flag Is Processed in update_util.cpp
The flag is evaluated during request parsing in src/mongo/db/update/update_util.cpp. The server sets the return behavior based on the new parameter:
requestOut->setReturnDocs((request.getNew().value_or(false)) ?
UpdateRequest::RETURN_NEW :
UpdateRequest::RETURN_OLD);
If request.getNew() evaluates to false (the default when omitted), the server configures the operation to return the old document (RETURN_OLD).
Mongoose-Specific Options for findoneandupdate
While the server controls the logic, Mongoose abstracts the option naming across different versions. You must use the correct option name for your Mongoose version to receive the updated document:
| Mongoose Version | Option to Return Updated Document |
|---|---|
| ≤ 5.x | { new: true } |
| ≥ 6.0 | { returnDocument: 'after' } or { returnOriginal: false } |
Mongoose 6.0+ aligns with the native MongoDB driver's modern API, introducing returnDocument and returnOriginal while maintaining backward compatibility with new: true.
Code Examples
For Mongoose 5.x and earlier, explicitly set new: true:
// Returns the updated document (Mongoose 5.x)
const doc = await Model.findOneAndUpdate(
{ _id: userId },
{ $set: { status: 'active' } },
{ new: true } // Required to get the post-update document
);
console.log(doc.status); // 'active'
For Mongoose 6.0+, use the modern syntax:
// Returns the updated document (Mongoose 6.0+)
const doc = await Model.findOneAndUpdate(
{ _id: userId },
{ $set: { status: 'active' } },
{ returnDocument: 'after' } // 'after' returns post-update, 'before' returns pre-update
);
Both approaches ensure the server returns the document after the update has been applied.
Common Pitfalls with findoneandupdate mongoose
Even with the correct syntax, several edge cases cause confusion:
Omitting the return option is the most frequent mistake. Without { new: true } or { returnDocument: 'after' }, the method resolves with the original document, making it appear as though the update failed.
Version mismatch occurs when developers upgrade Mongoose but continue using deprecated option names. While Mongoose 6+ accepts new: true, using returnDocument in Mongoose 5.x results in the option being ignored, defaulting to the old document.
Upsert operations behave differently when combined with return options. If upsert: true creates a new document and you omit the new flag (or equivalent), the method returns null rather than the inserted document because there was no "old" document to return.
Summary
findoneandupdate mongoosewraps MongoDB'sfindAndModifycommand, which defaults to returning the pre-update document.- The server logic in
src/mongo/db/commands/query_cmd/find_and_modify.cppandsrc/mongo/db/update/update_util.cppchecks thenewflag to determine whether to returnRETURN_OLDorRETURN_NEW. - Use
{ new: true }for Mongoose 5.x and earlier, or{ returnDocument: 'after' }/{ returnOriginal: false }for Mongoose 6.0+. - Always include the appropriate return option when you need the post-update document, especially during upserts.
Frequently Asked Questions
Why does findOneAndUpdate return null instead of the updated document?
When you perform an upsert (creating a document if it doesn't exist) and omit the new: true or returnDocument: 'after' option, findOneAndUpdate returns null because there was no existing document to return as the "old" version. Always set the return option to 'after' or true when using upserts to receive the newly created or updated document.
What is the difference between new: true and returnDocument: 'after'?
Both options achieve the same result—returning the document after the update has been applied—but they belong to different API versions. { new: true } is the legacy option used in Mongoose 5.x and earlier, while { returnDocument: 'after' } was introduced in Mongoose 6.0 to align with the native MongoDB driver's modern syntax. Mongoose 6+ supports both for backward compatibility.
Does findOneAndUpdate return the updated document by default in the latest MongoDB driver?
No, neither the native MongoDB driver nor Mongoose returns the updated document by default. The underlying findAndModify command defaults to new: false (or returnDocument: 'before'), meaning the server returns the document state prior to applying the update. You must explicitly set returnDocument: 'after' (or new: true in older versions) to receive the post-update document.
How can I verify that findOneAndUpdate actually performed the update if I don't get the new document back?
If you must use the default return behavior (receiving the old document), you can verify the update by checking the document in a subsequent query or by examining the command's metadata. However, the most efficient approach is to simply include the appropriate return option (new: true or returnDocument: 'after') in your initial call, eliminating the need for a second database round-trip and immediately confirming the updated state.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →