When I hover on the keyword 'function' the description says:
"(local function)(this: any, next: (err?: mongoose.CallbackError | undefined) => void): Promise<void>"
So does It return a Promise<void>
or a simple <void>
? I can't even understand what does this function returns? And to be honest I don't understand really well the concept of Promise<void>
...
userSchema.pre('save', async function (next) {
let user = this as UserDocument;
if(!user.isModified('password')){
return next();
}
const salt = await bcrypt.genSalt(config.get<number>('saltWorkFactor'));
const hash = await bcrypt.hash(user.password, salt);
user.password = hash;
return next();
})
CodePudding user response:
This question is really interesting. Your function returns a Promise<void>
, which is compatible with the void
return type that pre
is expecting, but Mongoose is quietly smart enough to know what to do with your Promise so you don't even have to call next
at all.
First some background:
void
has a special meaning in TypeScript to mean that the return value could be any value; the value is frequentlyundefined
(because that's what a function returns without areturn
statement) but it doesn't have to be. As in the TypeScript FAQ, this makes it convenient to accept or pass functions that return a value, even when that value is unused. If you need to supply a function with return typevoid
, you could pass back a function that returns astring
,Promise<void>
,Promise<SomeObject>
,null
,undefined
, or anything else.- All
async
functions return Promises, and this is no exception. APromise<number>
is a Promise that says that itsthen
function will receive anumber
; aPromise<void>
is a Promise that doesn't tell you anything about what itsthen
function returns, just that it'll do so unless it has an error tocatch
. - In Mongoose's types,
pre
takes aPreSaveMiddlewareFunction<T>
function, which is the type of the function you wrote. It accepts a function callednext
and returnsvoid
: Mongoose claims not to care what you return. Your middleware function is allowed to be asynchronous; when you're done you're expected to callnext
(with an error object, if you have one), and that call tonext
also returnsvoid
.
Your function passed to pre
returns type Promise<void>
: The function is async
so it absolutely returns a promise, and your return next();
means that the Promise resolves to whatever next
returns, which is defined as void
. You don't know what next
returns and shouldn't care about it. You don't even need to return next()
when it's at the end of the function: It's just a callback so you can tell Mongoose your middleware is done and report any errors.
So your async function
returns Promise<void>
, but that works with the definition of pre
: pre
doesn't care what kind of return value your function has (void
) as long as you call next
to indicate you're done.
But wait! Reporting that your asynchronous function is done and whether or not there were errors is exactly the problem that Promises were designed to solve, and the next
callback pattern is exactly the kind of pattern that Promises were designed to replace. If you're returning a Promise, why would you need to call next
at all when Mongoose can just watch the promise you return?
In fact, in Mongoose 5.x or later, that's exactly what happens: If the function you pass into pre
returns a Promise, then you can use that instead of calling next
. You can still call next
manually for compatibility's sake, but in your case you could delete return next()
and everything would keep working. See the middleware docs:
In mongoose 5.x, instead of calling
next()
manually, you can use a function that returns a promise. In particular, you can useasync/await
.schema.pre('save', function() { return doStuff(). then(() => doMoreStuff()); }); // Or, in Node.js >= 7.6.0: schema.pre('save', async function() { await doStuff(); await doMoreStuff(); });
The docs further explain why return next()
is a pattern at all:
If you use
next()
, thenext()
call does not stop the rest of the code in your middleware function from executing. Use the earlyreturn
pattern to prevent the rest of your middleware function from running when you callnext()
.const schema = new Schema(..); schema.pre('save', function(next) { if (foo()) { console.log('calling next!'); // `return next();` will make sure the rest of this function doesn't run /*return*/ next(); } // Unless you comment out the `return` above, 'after next' will print console.log('after next'); });
In summary, the return type of void
is compatible with the fact that you're returning a Promise<void>
, but it hides the fact that recent versions of Mongoose are smart enough to check whether you're returning a Promise and do the right thing without expecting a call to next
. They're two different styles that both work.
CodePudding user response:
Long answer short: It return a Promise<void>
Callbacks
To understand why, here are some details. First one must understand Callbacks in node.js. Callbacks are one of the basic structure/feature of how node.js works.
You could say that node.js is basically an Event-Driven Programming "framework" (most people will frown to the framework word...). That means that you tell node that in the event of a certain thing happening, it should do a certain action/function (callback).
For node to understand us, we normally give the callback function as a parameter to another function that will do the work of "listening to the event" and executing the callback that we give it. So it is not "us" that execute the callback, it is the event listener.
In your case,
userSchema.pre('save', async function (next) {
pre
is the function (a method in Mongoose's userSchema), save
is the event that one must react to, async function (next) {
is the callback or what must be done after the event.
You will note that your callback is returning next()
, but next()
returns void
, which mean that your callback is returning void
.
So why is it returning Promise<void>
?
The fact is that in your case, your callback is an async function. And every async functions will return a promise. It is an async function because it is awaiting another promise (two promises even) inside of it. They are hidden because of the await
const salt = await bcrypt.genSalt(config.get<number>('saltWorkFactor'));
const hash = await bcrypt.hash(user.password, salt);
Note: The bcrypt
methods are very expensive in terms of CPU and time (also a security feature among other things).
It also means that normally in your code
const hash = await bcrypt.hash(user.password, salt);
user.password = hash;
you couldn't have available "right away" the hash
value for the user.password
and, worse, you couldn't even know when it would come. Will your program stop and wait until bcrypt
finish its business?
If you have many async
functions, your program will be a great favourite for the slowest champion in the Olympics.
What is going on with those promises and how can we not be labelled as a geriatric program?
Promises
Here is a quick/long comment to try to explain the concept of promises.
In "normal" code, each lines of code is executed and "finished" before the next one. Ex: (with cooking)
- Combine the Butter and Sugar,
- Add Eggs One at a Time, etc.
Or in your code:
let user = this as UserDocument;
if(!user.isModified('password')){
return next();
}
A promise is a certain code that is executed but not finished before the next line of code. Ex:
- while the cake is in the oven (promise),
- you prepare the frosting,
- but you can't put it until the cake in baked (the "then" action of promises).
Note: Your code is using await
so there is no "explicit" then
method.
You will have many example of "promises" things in everyday life. you may have heard of asynchronous code = not one after the other, not in sync, ...
- Turning on an alarm to wake you in the morning,
then
you make the promise that you will not ignore it; - putting a reminder on the calendar
then
you make the promise that you will go to that job interview; etc.
All the while, you continue with your life after making those promises.
In code, a function that returns a promise will have a then
method where you tell the computer what to do when when the "alarms goes off".
It is usually written like this
mypromise().then(doThisThingFunction)
const continueWithMyLife = true
In this way the then
method is very similar to the callback of node.js. It is just expressed in a different way in the code and is not specific to node (callbacks are also not specific to node...).
One very important difference between them is that callbacks are something that the listener "do" and promises is something that resolves (hopefully) to a returning value.
Async/Await
Nowadays it is common to use async/await
. Fortunately/unfortunately it basically hides the asynchronous behaviour. Better flow of reading the code, but also much worse understanding of promises for new programmers.
After a await
, there is no then
method (Or you could say that the following line of code is the then
action). There is no "continuing with your life". There is only "waiting until the alarms goes off", So the next line after the await
is essentially the "get out of the bed action".
That is why, in your code, the hash
value is available in the next line. Basically in the "old way" to write promises
user.password = hash;
would be inside the then
function.
And that is also why it is returning Promise<void>
But still, all these analogies won't really help. The best is to try it in everyday code. There is nothing like experience to understand anything.