I'm adding a very basic login method to a Bookshelf User model in an ExpressJS app, but I can't catch the errors from the rejected promise that the login function in the User model returns. I'm looking at Bookshelf's example login in the docs at http://bookshelfjs.org/#Model-static-extend but that example uses Bluebird, whereas I'm trying to do the same with the built-in ES6 promises.
My login method in the User model:
userModel.js
function login(email, password) {
return new Promise((resolve, reject) => {
User.where('email', email)
.fetch({ require: true })
.then(user => {
bcrypt.compare(password, user.get('password'), (err, matched) => {
if (!matched) return reject(new Error('Password didn\'t match!'));
resolve(user);
});
});
});
The controller action that implements login and calls User.login
from the Bookshelf User
model:
usersAuthController.js
function logUserIn(req, res) {
new User().login(req.body.email, req.body.password)
.then(user => res.json({ message: 'Login succeeded!' }))
.catch(User.NotFoundError, () => res.status(404).json({ error: 'User not found!' }) // catch #1
.catch(err => res.status(401).json({ err: err.message })); // catch #2
}
My intention is that login()
can return a rejected promise when Bookshelf's User.fetch
method can't find the user with the given email. In that case, the .catch(User.NotFoundError ...)
(catch #1) line should catch it and return a 404. I also intend login()
to return a rejected Promise when bcrypt
determines that the password passed to login()
doesn't match the user's password, in which case the "catch-all" (catch #2) below the User.NotFoundError
catch statement should return a 401.
If I put in the incorrect password, the logUserIn()
controller action in the above code goes to catch #2 with the error message { error: "Cannot set property 'message' of undefined" }
instead of the message 'Password didn't match!'
message that I rejected in login()
. If I put in a nonexistent email, the response is never sent and the error Unhandled rejection CustomError: EmptyResponse
is thrown in the console. Only valid input works.
An attempt at fix: catching User.NotFoundError
directly in the model instead.
I moved catch #1 to the User model so that the login method now looks like:
userModel.js
function login(email, password) {
return new Promise((resolve, reject) => {
User.where('email', email)
.fetch({ require: true })
.then(user => {
bcrypt.compare(password, user.get('password'), (err, matched) => {
if (!matched) return reject(new Error('Password didn\'t match!'));
resolve(user);
});
})
.catch(User.NotFoundError, () => reject({ error: 'User not found!' }));
});
This way, I can catch both errors (incorrect password and nonexistent email) correctly, but this way I can't specify the status code in the controller. If the user with the given email couldn't be found, then it should return a 404, but if the password was incorrect, then it should return a 401, but both errors go to the catch-all (catch #2) in the controller action (which always returns a 401).
To solve this problem, in the User
model I could do .catch(User.NotFoundError, () => reject({ name: 'NotFoundError', message: 'User not found!' }))
and in the controller action I can check for what kind of error I'm getting with const statusCode = err.name === 'NotFoundError' ? 404 : 401
but that seems really messy and misses the point of having these .catch
statements.
Is there a way to catch the User.NotFoundError
from the model's login method and any other errors all in logInUser
? Why doesn't the setup I had at first work, the one with both catch
statements in usersAuthController.js, and what did the Cannot set property 'message' of undefined'
and CustomError: EmptyResponse
errors mean (does it have something to do with mixing up Bookshelf's Bluebird promises versus the built-in ES6 promises)? What's the best way to handle this?