The saveErrorIfNotFound option and $where property in mongoose 4.8 gives plugins a powerful new way to modify the behavior of save() . This feature may not seem as exciting as eachAsync() or the major perf improvements in 4.8, but I think it will help the community develop some handy abstractions on top of save() . How do these features work? Let's say you try to save a document that was deleted after you loaded it:

const M = mongoose.model( 'Test' , new Schema({ name: String })); M.create({}, function ( error, doc ) { M.remove({ _id: doc._id }, function ( ) { doc.name = 'test' ; doc.save( function ( error, doc, numModified ) { console .log(error, doc, numModified); }); }); }); null { name: 'test' , __v: 0 , _id: 589 cd5f176f15f362784d93a } 0

Mongoose doesn't report an error when it can't find the document to modify. The only indication is the third parameter to the callback, numModified , is 0. There's a couple problems with this setup, including, what happens if you're using promises? ES6 Promises can only resolve to a single value, so numModified is undefined and there is no way for you to tell if the underlying doc was saved or not.

mongoose.Promise = global.Promise; M.create({}, function ( error, doc ) { M.remove({ _id: doc._id }, function ( ) { doc.name = 'test' ; doc.save().then( function ( doc, numModified ) { console .log(doc, numModified); }); }); });

The saveErrorIfNotFound Option

If you set mongoose 4.8's saveErrorIfNotFound flag to true, you'll find that mongoose reports a DocumentNotFoundError :

const M = mongoose.model( 'Test' , new Schema({ name: String }, { saveErrorIfNotFound: true })); M.create({}, function ( error, doc ) { M.remove({ _id: doc._id }, function ( ) { doc.name = 'test' ; doc.save().catch(error => console .error(error)); }); });

This behavior works better with promises than the default numModified behavior, which was designed to work with a callback-based API. We're leaning toward setting saveErrorIfNotFound to true by default in 5.0 but haven't committed to it. If you have strong feelings one way or the other, please put your thoughts in this GitHub issue.

You'll notice that the DocumentNotFoundError message includes the query that was used to identify the document to save. Why is that? Because the $where property actually lets you change the underlying query that save() uses.

M.create({}).then(doc => { doc.name = 'test' ; doc.$where = { prop: 'test' }; doc.save().catch(error => console .error(error)); });

Modifying the $where object in conjunction with saveErrorIfNotFound lets you define a new class of abstractions around save() . Let's take a look at a few examples.

Example Uses for $where

Optimistic concurrency / timestamp checking const schema = new Schema({ name: String }, { saveErrorIfNotFound: true , timestamps: true }); schema.pre( 'save' , function ( next ) { this .$where = { updatedAt: this .updatedAt }; next(); }); const M = mongoose.model( 'Test' , schema); M.create({}). then(doc => M.updateOne({ _id: doc._id }, { $set: { name: 'test2' } }).then(() => doc)). then(doc => { doc.name = 'test3' ; return doc.save(); }). catch (error => console .error(error)); Enforcing soft deletes const schema = new Schema({ name: String , isDeleted: Boolean }, { saveErrorIfNotFound: true }); schema.pre( 'save' , function ( next ) { this .$where = { isDeleted: { $ne: true } }; next(); }); const M = mongoose.model( 'Test' , schema); M.create({}). then(doc => M.updateOne({ _id: doc._id }, { $set: { isDeleted: true } }).then(() => doc)). then(doc => { doc.name = 'test3' ; return doc.save(); }). catch (error => console .error(error)); Versioning (alternative to timestamp checking) const schema = new Schema({ name: String , _version: { type: Number , default : 0 } }, { saveErrorIfNotFound: true }); schema.pre( 'save' , function ( next ) { this .$where = { _version: this ._version }; ++ this ._version; next(); }); const M = mongoose.model( 'Test' , schema); M.create({}). then(doc => Promise .all([ M.findById(doc._id), M.findById(doc._id) ])). then(docs => { docs[ 0 ].name = 'test' ; return Promise .all([ docs[ 0 ].save(), docs[ 1 ] ]); }). then(docs => { docs[ 1 ].name = 'test2' ; return docs[ 1 ].save(); }). catch (error => console .error(error));

There's numerous other use cases for $where , like setting shard keys, enforcing uniqueness with .push() , and checking user permissions. I'm looking forward to seeing what other plugins you can come up with!

Moving On