Skip to content

Commit 5815535

Browse files
authored
Merge pull request #15599 from Automattic/vkarpov15/gh-11531
BREAKING CHANGE: remove support for callbacks in pre middleware
2 parents cce174c + f41de58 commit 5815535

27 files changed

+181
-314
lines changed

docs/middleware.md

Lines changed: 26 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -128,19 +128,17 @@ childSchema.pre('findOneAndUpdate', function() {
128128

129129
## Pre {#pre}
130130

131-
Pre middleware functions are executed one after another, when each
132-
middleware calls `next`.
131+
Pre middleware functions are executed one after another.
133132

134133
```javascript
135134
const schema = new Schema({ /* ... */ });
136-
schema.pre('save', function(next) {
135+
schema.pre('save', function() {
137136
// do stuff
138-
next();
139137
});
140138
```
141139

142-
In [mongoose 5.x](http://thecodebarbarian.com/introducing-mongoose-5.html#promises-and-async-await-with-middleware), instead of calling `next()` manually, you can use a
143-
function that returns a promise. In particular, you can use [`async/await`](http://thecodebarbarian.com/common-async-await-design-patterns-in-node.js.html).
140+
You can also use a function that returns a promise, including async functions.
141+
Mongoose will wait until the promise resolves to move on to the next middleware.
144142

145143
```javascript
146144
schema.pre('save', function() {
@@ -153,33 +151,22 @@ schema.pre('save', async function() {
153151
await doStuff();
154152
await doMoreStuff();
155153
});
156-
```
157-
158-
If you use `next()`, the `next()` call does **not** stop the rest of the code in your middleware function from executing. Use
159-
[the early `return` pattern](https://www.bennadel.com/blog/2323-use-a-return-statement-when-invoking-callbacks-especially-in-a-guard-statement.htm)
160-
to prevent the rest of your middleware function from running when you call `next()`.
161154

162-
```javascript
163-
const schema = new Schema({ /* ... */ });
164-
schema.pre('save', function(next) {
165-
if (foo()) {
166-
console.log('calling next!');
167-
// `return next();` will make sure the rest of this function doesn't run
168-
/* return */ next();
169-
}
170-
// Unless you comment out the `return` above, 'after next' will print
171-
console.log('after next');
155+
schema.pre('save', function() {
156+
// Will execute **after** `await doMoreStuff()` is done
172157
});
173158
```
174159

175160
### Use Cases
176161

177-
Middleware are useful for atomizing model logic. Here are some other ideas:
162+
Middleware is useful for atomizing model logic. Here are some other ideas:
178163

179164
* complex validation
180165
* removing dependent documents (removing a user removes all their blogposts)
181166
* asynchronous defaults
182167
* asynchronous tasks that a certain action triggers
168+
* updating denormalized data on other documents
169+
* saving change records
183170

184171
### Errors in Pre Hooks {#error-handling}
185172

@@ -189,11 +176,9 @@ and/or reject the returned promise. There are several ways to report an
189176
error in middleware:
190177

191178
```javascript
192-
schema.pre('save', function(next) {
179+
schema.pre('save', function() {
193180
const err = new Error('something went wrong');
194-
// If you call `next()` with an argument, that argument is assumed to be
195-
// an error.
196-
next(err);
181+
throw err;
197182
});
198183

199184
schema.pre('save', function() {
@@ -222,9 +207,6 @@ myDoc.save(function(err) {
222207
});
223208
```
224209

225-
Calling `next()` multiple times is a no-op. If you call `next()` with an
226-
error `err1` and then throw an error `err2`, mongoose will report `err1`.
227-
228210
## Post middleware {#post}
229211

230212
[post](api.html#schema_Schema-post) middleware are executed *after*
@@ -373,16 +355,13 @@ const User = mongoose.model('User', userSchema);
373355
await User.findOneAndUpdate({ name: 'John' }, { $set: { age: 30 } });
374356
```
375357

376-
For document middleware, like `pre('save')`, Mongoose passes the 1st parameter to `save()` as the 2nd argument to your `pre('save')` callback.
377-
You should use the 2nd argument to get access to the `save()` call's `options`, because Mongoose documents don't store all the options you can pass to `save()`.
358+
Mongoose also passes the 1st parameter to the hooked function, like `save()`, as the 1st argument to your `pre('save')` function.
359+
You should use the argument to get access to the `save()` call's `options`, because Mongoose documents don't store all the options you can pass to `save()`.
378360

379361
```javascript
380362
const userSchema = new Schema({ name: String, age: Number });
381-
userSchema.pre('save', function(next, options) {
363+
userSchema.pre('save', function(options) {
382364
options.validateModifiedOnly; // true
383-
384-
// Remember to call `next()` unless you're using an async function or returning a promise
385-
next();
386365
});
387366
const User = mongoose.model('User', userSchema);
388367

@@ -513,10 +492,9 @@ await Model.updateOne({}, { $set: { name: 'test' } });
513492

514493
## Error Handling Middleware {#error-handling-middleware}
515494

516-
Middleware execution normally stops the first time a piece of middleware
517-
calls `next()` with an error. However, there is a special kind of post
518-
middleware called "error handling middleware" that executes specifically
519-
when an error occurs. Error handling middleware is useful for reporting
495+
Middleware execution normally stops the first time a piece of middleware throws an error, or returns a promise that rejects.
496+
However, there is a special kind of post middleware called "error handling middleware" that executes specifically when an error occurs.
497+
Error handling middleware is useful for reporting
520498
errors and making error messages more readable.
521499

522500
Error handling middleware is defined as middleware that takes one extra
@@ -553,13 +531,13 @@ errors.
553531

554532
```javascript
555533
// The same E11000 error can occur when you call `updateOne()`
556-
// This function **must** take 4 parameters.
534+
// This function **must** take exactly 3 parameters.
557535

558-
schema.post('updateOne', function(passRawResult, error, res, next) {
536+
schema.post('updateOne', function(error, res, next) {
559537
if (error.name === 'MongoServerError' && error.code === 11000) {
560-
next(new Error('There was a duplicate key error'));
538+
throw new Error('There was a duplicate key error');
561539
} else {
562-
next(); // The `updateOne()` call will still error out.
540+
next();
563541
}
564542
});
565543

@@ -570,9 +548,8 @@ await Person.create(people);
570548
await Person.updateOne({ name: 'Slash' }, { $set: { name: 'Axl Rose' } });
571549
```
572550

573-
Error handling middleware can transform an error, but it can't remove the
574-
error. Even if you call `next()` with no error as shown above, the
575-
function call will still error out.
551+
Error handling middleware can transform an error, but it can't remove the error.
552+
Even if the error handling middleware succeeds, the function call will still error out.
576553

577554
## Aggregation Hooks {#aggregate}
578555

@@ -598,10 +575,9 @@ pipeline from middleware.
598575

599576
## Synchronous Hooks {#synchronous}
600577

601-
Certain Mongoose hooks are synchronous, which means they do **not** support
602-
functions that return promises or receive a `next()` callback. Currently,
603-
only `init` hooks are synchronous, because the [`init()` function](api/document.html#document_Document-init)
604-
is synchronous. Below is an example of using pre and post init hooks.
578+
Certain Mongoose hooks are synchronous, which means they do **not** support functions that return promises.
579+
Currently, only `init` hooks are synchronous, because the [`init()` function](api/document.html#document_Document-init) is synchronous.
580+
Below is an example of using pre and post init hooks.
605581

606582
```acquit
607583
[require:post init hooks.*success]

docs/migrating_to_9.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -185,12 +185,14 @@ const { promiseOrCallback } = require('mongoose');
185185
promiseOrCallback; // undefined in Mongoose 9
186186
```
187187

188-
## In `isAsync` middleware `next()` errors take priority over `done()` errors
188+
## `isAsync` middleware no longer supported
189189

190-
Due to Mongoose middleware now relying on promises and async/await, `next()` errors take priority over `done()` errors.
191-
If you use `isAsync` middleware, any errors in `next()` will be thrown first, and `done()` errors will only be thrown if there are no `next()` errors.
190+
Mongoose 9 no longer supports `isAsync` middleware. Middleware functions that use the legacy signature with both `next` and `done` callbacks (i.e., `function(next, done)`) are not supported. We recommend middleware now use promises or async/await.
191+
192+
If you have code that uses `isAsync` middleware, you must refactor it to use async functions or return a promise instead.
192193

193194
```javascript
195+
// ❌ Not supported in Mongoose 9
194196
const schema = new Schema({});
195197

196198
schema.pre('save', true, function(next, done) {
@@ -214,8 +216,16 @@ schema.pre('save', true, function(next, done) {
214216
25);
215217
});
216218

217-
// In Mongoose 8, with the above middleware, `save()` would error with 'first done() error'
218-
// In Mongoose 9, with the above middleware, `save()` will error with 'second next() error'
219+
// ✅ Supported in Mongoose 9: use async functions or return a promise
220+
schema.pre('save', async function() {
221+
execed.first = true;
222+
await new Promise(resolve => setTimeout(resolve, 5));
223+
});
224+
225+
schema.pre('save', async function() {
226+
execed.second = true;
227+
await new Promise(resolve => setTimeout(resolve, 25));
228+
});
219229
```
220230

221231
## Removed `skipOriginalStackTraces` option

lib/helpers/timestamps/setupTimestamps.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,13 @@ module.exports = function setupTimestamps(schema, timestamps) {
4242

4343
schema.add(schemaAdditions);
4444

45-
schema.pre('save', function timestampsPreSave(next) {
45+
schema.pre('save', function timestampsPreSave() {
4646
const timestampOption = get(this, '$__.saveOptions.timestamps');
4747
if (timestampOption === false) {
48-
return next();
48+
return;
4949
}
5050

5151
setDocumentTimestamps(this, timestampOption, currentTime, createdAt, updatedAt);
52-
53-
next();
5452
});
5553

5654
schema.methods.initializeTimestamps = function() {
@@ -88,7 +86,7 @@ module.exports = function setupTimestamps(schema, timestamps) {
8886
schema.pre('updateOne', opts, _setTimestampsOnUpdate);
8987
schema.pre('updateMany', opts, _setTimestampsOnUpdate);
9088

91-
function _setTimestampsOnUpdate(next) {
89+
function _setTimestampsOnUpdate() {
9290
const now = currentTime != null ?
9391
currentTime() :
9492
this.model.base.now();
@@ -105,6 +103,5 @@ module.exports = function setupTimestamps(schema, timestamps) {
105103
replaceOps.has(this.op)
106104
);
107105
applyTimestampsToChildren(now, this.getUpdate(), this.model.schema);
108-
next();
109106
}
110107
};

lib/model.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2914,7 +2914,6 @@ Model.insertMany = async function insertMany(arr, options) {
29142914
await this._middleware.execPost('insertMany', this, [arr], { error });
29152915
}
29162916

2917-
29182917
options = options || {};
29192918
const ThisModel = this;
29202919
const limit = options.limit || 1000;

lib/plugins/validateBeforeSave.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
module.exports = function validateBeforeSave(schema) {
88
const unshift = true;
9-
schema.pre('save', false, async function validateBeforeSave(_next, options) {
9+
schema.pre('save', false, async function validateBeforeSave(options) {
1010
// Nested docs have their own presave
1111
if (this.$isSubdocument) {
1212
return;

lib/schema.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2081,23 +2081,21 @@ Schema.prototype.queue = function(name, args) {
20812081
*
20822082
* const toySchema = new Schema({ name: String, created: Date });
20832083
*
2084-
* toySchema.pre('save', function(next) {
2084+
* toySchema.pre('save', function() {
20852085
* if (!this.created) this.created = new Date;
2086-
* next();
20872086
* });
20882087
*
2089-
* toySchema.pre('validate', function(next) {
2088+
* toySchema.pre('validate', function() {
20902089
* if (this.name !== 'Woody') this.name = 'Woody';
2091-
* next();
20922090
* });
20932091
*
20942092
* // Equivalent to calling `pre()` on `find`, `findOne`, `findOneAndUpdate`.
2095-
* toySchema.pre(/^find/, function(next) {
2093+
* toySchema.pre(/^find/, function() {
20962094
* console.log(this.getFilter());
20972095
* });
20982096
*
20992097
* // Equivalent to calling `pre()` on `updateOne`, `findOneAndUpdate`.
2100-
* toySchema.pre(['updateOne', 'findOneAndUpdate'], function(next) {
2098+
* toySchema.pre(['updateOne', 'findOneAndUpdate'], function() {
21012099
* console.log(this.getFilter());
21022100
* });
21032101
*

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"type": "commonjs",
2121
"license": "MIT",
2222
"dependencies": {
23-
"kareem": "git+https://github.com/mongoosejs/kareem.git#vkarpov15/v3",
23+
"kareem": "git+https://github.com/mongoosejs/kareem.git#vkarpov15/remove-isasync",
2424
"mongodb": "~6.18.0",
2525
"mpath": "0.9.0",
2626
"mquery": "5.0.0",

test/aggregate.test.js

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -886,9 +886,9 @@ describe('aggregate: ', function() {
886886
const s = new Schema({ name: String });
887887

888888
let called = 0;
889-
s.pre('aggregate', function(next) {
889+
s.pre('aggregate', function() {
890890
++called;
891-
next();
891+
return Promise.resolve();
892892
});
893893

894894
const M = db.model('Test', s);
@@ -902,9 +902,9 @@ describe('aggregate: ', function() {
902902
it('setting option in pre (gh-7606)', async function() {
903903
const s = new Schema({ name: String });
904904

905-
s.pre('aggregate', function(next) {
905+
s.pre('aggregate', function() {
906906
this.options.collation = { locale: 'en_US', strength: 1 };
907-
next();
907+
return Promise.resolve();
908908
});
909909

910910
const M = db.model('Test', s);
@@ -920,9 +920,9 @@ describe('aggregate: ', function() {
920920
it('adding to pipeline in pre (gh-8017)', async function() {
921921
const s = new Schema({ name: String });
922922

923-
s.pre('aggregate', function(next) {
923+
s.pre('aggregate', function() {
924924
this.append({ $limit: 1 });
925-
next();
925+
return Promise.resolve();
926926
});
927927

928928
const M = db.model('Test', s);
@@ -980,8 +980,8 @@ describe('aggregate: ', function() {
980980
const s = new Schema({ name: String });
981981

982982
const calledWith = [];
983-
s.pre('aggregate', function(next) {
984-
next(new Error('woops'));
983+
s.pre('aggregate', function() {
984+
throw new Error('woops');
985985
});
986986
s.post('aggregate', function(error, res, next) {
987987
calledWith.push(error);
@@ -1003,9 +1003,9 @@ describe('aggregate: ', function() {
10031003

10041004
let calledPre = 0;
10051005
let calledPost = 0;
1006-
s.pre('aggregate', function(next) {
1006+
s.pre('aggregate', function() {
10071007
++calledPre;
1008-
next();
1008+
return Promise.resolve();
10091009
});
10101010
s.post('aggregate', function(res, next) {
10111011
++calledPost;
@@ -1030,9 +1030,9 @@ describe('aggregate: ', function() {
10301030

10311031
let calledPre = 0;
10321032
const calledPost = [];
1033-
s.pre('aggregate', function(next) {
1033+
s.pre('aggregate', function() {
10341034
++calledPre;
1035-
next();
1035+
return Promise.resolve();
10361036
});
10371037
s.post('aggregate', function(res, next) {
10381038
calledPost.push(res);
@@ -1295,11 +1295,10 @@ describe('aggregate: ', function() {
12951295
it('cursor() errors out if schema pre aggregate hook throws an error (gh-15279)', async function() {
12961296
const schema = new Schema({ name: String });
12971297

1298-
schema.pre('aggregate', function(next) {
1298+
schema.pre('aggregate', function() {
12991299
if (!this.options.allowed) {
13001300
throw new Error('Unauthorized aggregate operation: only allowed operations are permitted');
13011301
}
1302-
next();
13031302
});
13041303

13051304
const Test = db.model('Test', schema);

test/docs/discriminators.test.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,17 +164,15 @@ describe('discriminator docs', function() {
164164

165165
const eventSchema = new mongoose.Schema({ time: Date }, options);
166166
let eventSchemaCalls = 0;
167-
eventSchema.pre('validate', function(next) {
167+
eventSchema.pre('validate', function() {
168168
++eventSchemaCalls;
169-
next();
170169
});
171170
const Event = mongoose.model('GenericEvent', eventSchema);
172171

173172
const clickedLinkSchema = new mongoose.Schema({ url: String }, options);
174173
let clickedSchemaCalls = 0;
175-
clickedLinkSchema.pre('validate', function(next) {
174+
clickedLinkSchema.pre('validate', function() {
176175
++clickedSchemaCalls;
177-
next();
178176
});
179177
const ClickedLinkEvent = Event.discriminator('ClickedLinkEvent',
180178
clickedLinkSchema);

0 commit comments

Comments
 (0)