I have been working on my first node.js backend and I am trying to refactor the login function, but i am not understanding something about promise chaining and error handling. I am hoping someone can help point out what I am missing here.
I have read about promise chaining and I understand that one catch should be able to handle any error in the chain, but I cant seem to get it to work without having an individual .catch for every .then
Here is my userLogin function.
const loginUser = handleAsync(async (req, res, next) => {
let userEmail = req.body.email;
let submittedPassword = req.body.password;
userLogin(userEmail, submittedPassword)
.then((userObject) => {
res.json(userObject);
})
.catch((err) => {
res.status(401).send(err);
console.log("any error inside userLogin: " + err);
});
});
async function userLogin(email, submittedPassword) {
console.log("in user login");
return new Promise(function (resolve, reject) {
getUserAccountByEmail(email)
.then((userAccountResults) => {
let email = userAccountResults[0].email;
let storedPassword = userAccountResults[0].password;
//the user exists check the password
//the user exists. Now Check password.
checkUserPassword(submittedPassword, storedPassword)
.then((passwordIsAMatch) => {
//The password is a match. Now Generate JWT Token.
generateJWTToken(email)
.then((tokens) => {
//build json object with user information and tokens.
createUserDataObject(tokens, userAccountResults)
.then((userObject) => {
//send the user object to the front end.
resolve(userObject);
})
.catch((err) => {
reject(err);
});
})
.catch((err) => {
reject(err);
});
})
.catch((err) => {
reject(err);
});
})
.catch((err) => {
reject(err);
});
});
}
And here is one of the functions that is a part of the chain
function getUserAccountByEmail(email) {
return new Promise(function (resolve, reject) {
logger.info("Looking up user email");
const users = User.findAll({
where: {
email: email,
},
limit: 1,
attributes: [
"email",
"password",
"userId",
"stripeConnectAccountId",
"isStripeAccountSet",
"stripeCustomerId",
],
})
.then((userResults) => {
if (doesUserExist(userResults)) {
resolve(userResults);
} else {
console.log("user doesnt exist in getuseraccount");
reject("User Email Does Not Exist In Database");
}
})
.catch((error) => {
console.log("Error Accessing Database: UserAccountByEmail");
logger.error("Error in getUserAccountByEmail: " + error);
});
});
}
Any help would be appreciated. Thanks
I took jfriend00 's advice and refactored using await instead of nesting.
async function userLogin(email, submittedPassword) {
return new Promise(async function (resolve, reject) {
try {
//Get User Account Information
const userAccountResults = await getUserAccountByEmail(email);
//Password Authentication
let storedPassword = userAccountResults[0].password;
const passwordIsAMatch = await checkUserPassword(
submittedPassword,
storedPassword
);
//Generate JWT Tokens
const tokens = await generateJWTToken(email);
//Prepare user data JSON for sending to the frontend.
const userData = await createUserDataObject(tokens, userAccountResults);
resolve(userData);
} catch (error) {
reject(error);
}
});
}
Related
I'm trying to execute a function after another function (API call) has returned its result. The problem is, the program always ends up executing the second one before the first one has given the result.
The thing is, I need to place a contact email on a Mailing List using Mailjet, but first I have to create that contact. So, the contact creation works, but not the placement on the list, as this function is executed before the contact creation finishes.
I tried multiple things for some days, mostly using async/await, but I still don't get my head around it.
Here's my code:
routes/index.js
router.post('/', async (req, res, next) => {
const { email, name } = req.body;
const mktListId = process.env.MAILJET_ID_MARKETING;
try {
const contactCreated = await createContact(email, name);
addEmailToList(email, mktListId);
res.status(201).send({ message: 'Email Successfully subscribed to Marketing List' });
} catch (err) {
res.status(400).json({
status: 'fail',
message: err,
});
}
});
function createContact(email, name) {
const mailjet = require('node-mailjet').connect(
process.env.MAILJET_MASTER_APIPUBLIC,
process.env.MAILJET_MASTER_APISECRET
);
const request = mailjet.post('contact', { version: 'v3' }).request({
IsExcludedFromCampaigns: 'true',
Name: `${name}`,
Email: `${email}`,
});
request
.then(result => {
console.log('result mailjet create contact', result.body);
})
.catch(err => {
console.log('error mailjet create contact', err.statusCode, err.ErrorMessage);
});
}
function addEmailToList(email, listId) {
const mailjet = require('node-mailjet').connect(
process.env.MAILJET_MASTER_APIPUBLIC,
process.env.MAILJET_MASTER_APISECRET
);
const request = mailjet.post('listrecipient', { version: 'v3' }).request({
IsUnsubscribed: 'true',
ContactAlt: `${email}`,
ListID: `${listId}`,
});
request
.then(result => {
console.log('result mailjet add to list', result.body);
})
.catch(err => {
console.log('error mailjet add to list', err.statusCode, err.ErrorMessage);
});
}
Any help with be much appreciated. Thank you!
Without a promise, await doesn't really do anything.
await new Promise (resolve => {
console.log ("A");
resolve ();
});
await new Promise (resolve => {
console.log ("B");
resolve ();
});
await new Promise (resolve => {
console.log ("C");
resolve ();
});
The create function needs to look more like:
function createContact(email, name) {
return new Promise ((resolve, reject) => {
const mailjet = require('node-mailjet').connect(
process.env.MAILJET_MASTER_APIPUBLIC,
process.env.MAILJET_MASTER_APISECRET
);
const request = mailjet.post('contact', { version: 'v3' }).request({
IsExcludedFromCampaigns: 'true',
Name: `${name}`,
Email: `${email}`,
});
request
.then(result => {
console.log('result mailjet create contact', result.body);
resolve ();
})
.catch(err => {
console.log('error mailjet create contact', err.statusCode, err.ErrorMessage);
reject ();
});
});
}
For future reference, this is the clean finished and refactored code:
routes/index.js
const express = require('express');
const router = express.Router();
const emailController = require('../controllers/emailController');
router.post('/', subscriberValidationRules(), validate, emailController.create);
Then we put the router logic on:
controllers/emailController.js
const { createContact, addEmailToList } = require('../helpers/addEmailToList');
const create = async (req, res, next) => {
const { email, name } = req.body;
const mktListId = process.env.MAILJET_ID_MARKETING;
try {
await createContact(email, name);
await addEmailToList(email, mktListId);
return res.status(201).send({ message: 'Email Successfully subscribed to Marketing List' });
} catch (err) {
res.status(400).json({
status: 'fail',
message: err,
});
}
}
module.exports = {
create,
};
And then our functions on:
helpers/addEmailToList.js
const createContact = async (email, name) => {
try {
const mailjet = require('node-mailjet').connect(
process.env.MAILJET_MASTER_APIPUBLIC,
process.env.MAILJET_MASTER_APISECRET,
)
const { body } = await mailjet.post('contact', { version: 'v3' }).request({
IsExcludedFromCampaigns: 'true',
Name: `${name}`,
Email: `${email}`,
})
console.info('result mailjet create contact', body)
return body
} catch (err) {
console.info('error mailjet create contact', err.statusCode, err.ErrorMessage)
}
}
const addEmailToList = async (email, listId) => {
const mailjet = require('node-mailjet').connect(
process.env.MAILJET_MASTER_APIPUBLIC,
process.env.MAILJET_MASTER_APISECRET
);
try {
const { body } = await mailjet.post('listrecipient', { version: 'v3' }).request({
IsUnsubscribed: 'true',
ContactAlt: `${email}`,
ListID: `${listId}`,
});
console.info('result mailjet add to list', body);
return body;
} catch (err) {
console.info('error mailjet add to list', err.statusCode, err.ErrorMessage);
}
};
module.exports = {
createContact,
addEmailToList,
};
//Update the user's email endpoint.
apiRouter.post('/update-email', [
check('newEmail')
.isEmail().withMessage('Please Insert a valid Email')
.custom(newEmail=> {
db.query(`SELECT user_id FROM users WHERE email = ?`, newEmail,(err, res)=> {
if(res.length > 0) throw new Error('Email is already registered.');
});
})
], (req, res)=> {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json(errors);
} else {
const newEmail = req.body.newEmail;
const id = req.body.id;
userCRUDfuncs.updateEmail(id, newEmail, db, (err=> {
if(!err) {
return res.status(201).send();
} else {
return res.status(404).send();
}
}));
}
})
This code returns the following error: "throw err; // Rethrow non-MySQL errors".
I have tried using callbacks and Promises but I can never throw the error outside the query function. I could not find a way to signal the outside function to throw the error.
I really appreciate your help on this .
Thanks in advance.
Make your own custom validator and wrap your query inside a promise
.custom((value, {req}) => {
return new Promise((resolve, reject) => {
db.query(`SELECT user_id FROM users WHERE email = ?`, req.body.email,(err,res)=>
{
if(err) {
reject(new Error('Server Error')
}
if(res.length > 0) {
reject(new Error('E-mail already in use'))
}
resolve(true)
});
});
})
I´m using this code to retrieve my contacts list inside a firebase function following the
(https://github.com/googleapis/google-api-nodejs-client/blob/master/samples/people/contacts.js) example
const { google } = require('googleapis');
const clientAuth = require('./clientAuth');
exports.getGoogleContacts = functions.https.onCall(async (data, context) => {
console.log('getGoogleContacts- init function')
const contacts = google.people({
version: 'v1',
auth: clientAuth.oAuth2Client,
});
return new Promise((resolve, reject) => {
clientAuth
.authenticate(['https://www.googleapis.com/auth/contacts'])
.then(async () => {
console.log('after client auth')
contacts.people.connections.list({
resourceName: "people/me",
pageSize:200,
personFields: ['addresses',
'ageRanges',
'biographies',
'birthdays',
'braggingRights',
'coverPhotos'], // ... and many other fields
}, function (err, response) {
if (err) {
console.log('contacts.people.connections error')
console.log(err)
reject(new Error(err))
} else if (response) {
console.log('contacts.people.connections response')
console.log(response)
if (response.pageToken) {
// how could I continue to retrieve next page of contacts?
}
resolve(response)
}
})
})
})
})
If there is a nextPageToken, there is no working example I´ve was able to found.
edited - this code was able to solve the pagination with array concatenation
I was able to come up with this solution, even though now I´m facing a problem of pushing to the connections array... It´s not working
const listOptions = {
resourceName: "people/me",
pageSize: 200,
personFields: ['addre...']
}
async function getConnectionsList(contacts, nextPageToken) {
if (!nextPageToken) {
return contacts.people.connections.list(listOptions)
} else {
listOptions.pageToken = nextPageToken
return contacts.people.connections.list(listOptions)
}
}
let response = await getConnectionsList(contacts)
let nextPage = response.data.nextPageToken
let connections = response.data.connections
while (nextPage) {
nextPage = await getConnectionsList(contacts, nextPage)
connections.push(nextPage.data.connections) // not working
connections.concat(nextPage.data.connections) // also not working...
nextPage = nextPage.data.nextPageToken
console.log('hasNextPage?', nextPage)
}
console.log('connections',connections)
resolve(connections)
This question already has answers here:
Why is my variable unaltered after I modify it inside of a function? - Asynchronous code reference
(7 answers)
Closed 4 years ago.
I have some code like this:
let addUser = (req, res) => {
let user;
let message = "";
let status = 200;
bcrypt.hash(password, salt_rounds, (err, hash) => {
user = new user({
...
password: hash,
});
user.save(err => {
if(err) {
message = "No success";
status = 400;
} else {
message = "success";
}
});
res.status(status).json({
message: message,
});
});
};
However, the message variable is undefined when the response is sent. I've been searching for a while now and, unfortunately, haven't been able to find a solution similar to this problem.
Ultimately, what I would like is to be able to update the message, status, etc. variables within the bcrypt/.save closures. I have tried using callbacks and resolving promises which I'm assuming didn't work due to naivety. Regardless, any solution would be appreciated!
Note: I would not like to use any other libraries to solve this problem (which I really doubt is required in the first place)
Thanks in advance!
Change user.save into something that returns a Promise, and then call .then on that Promise:
let addUser = (req, res) => {
let user;
let status = 200;
bcrypt.hash(password, salt_rounds, (err, hash) => {
user = new user({
password: hash,
});
const savePromise = new Promise((resolve, reject) => {
user.save(err => {
if (err) {
status = 400;
reject("No success");
} else {
resolve("success");
}
});
});
savePromise.then(message =>
res.status(status).json({ message })
).catch(message =>
res.status(status).json({ message })
);
});
};
Try this, put message key in double quotes
let addUser = (req, res) => {
let user;
let message = "";
let status = 200;
bcrypt.hash(password, salt_rounds, (err, hash) => {
user = new user({
...
password: hash,
});
user.save(err => {
if(err) {
message = "No success";
status = 400;
} else {
message = "success";
}
});
res.status(status).json({
"message": message,
});
});
};
I have a route that allows a user to reset their password by sending them an email. Standard procedure for most websites. In this route, I import the async npm module and use the waterfall method so that I can handle the asynchronous nature of multiple functions. I'm still having a bit of trouble understanding promises, but I'm trying to replace waterfall with a promise or promise chain.
How could I refactor this route with a promise? Here are the steps contained in this route that is currently split up into 4 functions with waterfall.
First the route creates a reset token
Search for user based on email
2.5. If user is found, save user, otherwise return 404
Send email to user containing a reset url
Return a status of 200.
app.post('/forgotPassword', function(req, res, next) {
waterfall([
// generate reset token
function(done) {
crypto.randomBytes(20, function(err, buf) {
var token = buf.toString('hex');
done(err, token);
});
},
function(token, done) {
// search for user with the given email
User.findOne({ email: req.body.email }, function(err, user) {
// check to see if the user exists
if (!user) {
// user doesn't exist in database
return res.status(404).send();
}
// user exists, assign token with expiration date
user.resetPasswordToken = token;
user.resetPasswordExpires = Date.now() + 3600000; // 1 hour from now
// save the user model with the newly added
// token and expiration date
user.save(function(err) {
done(err, token, user);
});
});
},
function(token, user, done) {
var smtpTransport = nodemailer.createTransport('SMTP', {
service: 'SendGrid',
auth: {
user: config.sendgridUser,
pass: config.sendgridPassword
}
});
var mailOptions = {
to: user.email,
from: 'email#school.edu',
subject: 'Password Reset',
text: `Hello etc etc`,
smtpTransport.sendMail(mailOptions, function(err) {
done(err, 'done');
});
}],
function(err) {
// handle error
if (err) return next(err);
res.status(200).send();
});
}); // end POST route '/forgotPassword'
Promise is a very powerful tool. It can be hard to understand it at the beginning, but totally worth the effort! Please let me know if you have any doubts :)
app.post('/forgotPassword', function(req, res, next)
{
new Promise((resolve, reject) =>
{
// generate reset token
crypto.randomBytes(20, (err, buf) =>
{
if(err)
return reject(err);
const token = buf.toString('hex');
resolve(token);
});
})
.then((token) =>
{
return new Promise((resolve, reject) => {
// search for user with the given email
User.findOne({ email: req.body.email }, (err, user) =>
{
if (!user)
return reject(404);
// user exists, assign token with expiration date
user.resetPasswordToken = token;
user.resetPasswordExpires = Date.now() + 3600000; // 1 hour from now
// save the user model with the newly added
// token and expiration date
user.save(function(err) {
if(err)
return reject(err);
resolve(user);
});
});
});
})
.then((user) =>
{
return new Promise((resolve, reject) =>
{
const smtpTransport = nodemailer.createTransport('SMTP',
{
service: 'SendGrid',
auth: {
user: config.sendgridUser,
pass: config.sendgridPassword
}
});
const mailOptions = {
to: user.email,
from: 'email#school.edu',
subject: 'Password Reset',
text: `Hello etc etc`
};
smtpTransport.sendMail(mailOptions, (err) =>
{
if(err)
return reject(err);
resolve();
});
});
})
.then(() => res.sendStatus(200))
.catch((err) =>
{
//check if the error is the one from the DB where the user was not found
if(err == 404) {
return res.sendStatus(404);
}
return res.status(500).send(err);
});
});
bluebird is one of the most popular promise library.
and it offers promisify function to convert callback hell to promise.
please read this document and play with it.
http://bluebirdjs.com/docs/working-with-callbacks.html
This answer presumes 4 completely sequential steps:
(Function () {
Return Promise (function (resolve, promise) {
// try to do some stuff
if (success) {
Resolve ("pass this value on to next link in the chain");
}
Reject();
})()
.then (function (value) {
// do next step
Return "pass this value on to next link in the chain";
.then (function (value) {
// do next step
Return "pass this value on to next link in the chain";
.catch (function (error) {
// handle any reject or any error in the chain
}
You could in relation to the individual .then also choose to return a Promise. Beware: any errors in the .catch block will be swallowed.
This is just an example with ES6 promise but working code. You can refactor it further to have a clear reusable code. You can replace ES5 functions with ES6 arrow functions.
app.post('/forgotPassword', function(req, res, next) {
var catch = function(err){
return next(err);
}:
var error404 = function(){
return res.status(404).send();
};
var success = function(){
return res.status(200).send();
};
var step1 = new Promise(function(resolve, reject){
crypto.randomBytes(20, function(err, buf) {
if(err){
reject(err);
} else {
var token = buf.toString('hex');
resolve(token);
}
});
});
var step2 = function(token) {
// search for user with the given email
User.findOne({ email: req.body.email }, function(err, user) {
// check to see if the user exists
if (!user) {
// user doesn't exist in database
return {error404: true};
}
// user exists, assign token with expiration date
user.resetPasswordToken = token;
user.resetPasswordExpires = Date.now() + 3600000; // 1 hour from now
// save the user model with the newly added
// token and expiration date
user.save(function(err) {
return { error: err, token: token, user: user });
});
});
};
var step3 = function(obj) {
if(obj.error){
return catch(obj.error);
} else if(obj.error404) {
return error404();
} else {
var token = obj.token, user = obj.user;
var smtpTransport = nodemailer.createTransport('SMTP', {
service: 'SendGrid',
auth: {
user: config.sendgridUser,
pass: config.sendgridPassword
}
});
var mailOptions = {
to: user.email,
from: 'email#school.edu',
subject: 'Password Reset',
text: `Hello etc etc`,
smtpTransport.sendMail(mailOptions, function(err) {
if(err){
return catch(err);
} else {
return success();
}
});
}
};
step1.then(step2, catch).then(step3);
}); // end POST route '/forgotPassword'
Promises work best when you're dealing with a library where methods already return them, or a library like bluebird that lets you promisify existing callback APIs, otherwise it's a lot of conversions.
If you're still set on refactoring with plain es6, put your promise-wrappers at the lowest possible level, for best effect and cleanest error handling, basically promisify manually:
let convert = (resolve, reject) => (err, res) => err ? reject(err) : resolve(res);
crypto.randomBytesAsync = n=>new Promise((y,n)=>crypto.randomBytes(n,convert(y,n)));
User.findOneAsync = u => new Promise((y, n) => User.findOne(u, convert(y, n)));
User.prototype.saveAsync = () => new Promise((y, n) => this.save(convert(y, n)));
Then use them like this:
app.post('/forgotPassword', function(req, res, next) {
crypto.randomBytesAsync(20).then(buf => {
var token = buf.toString('hex');
// search for user with the given email
return User.findOneAsync({ email: req.body.email }).then(user => {
// check to see if the user exists
if (!user) {
// user doesn't exist in database
res.status(404).send();
throw;
}
// user exists, assign token with expiration date
user.resetPasswordToken = token;
user.resetPasswordExpires = Date.now() + 3600000; // 1 hour from now
// save the user model with the newly added
// token and expiration date
return user.saveAsync().then(() => {
var smtpTransport = nodemailer.createTransport('SMTP', {
service: 'SendGrid',
auth: { user: config.sendgridUser, pass: config.sendgridPassword }
});
smtpTransport.sendMailAsync =
o => new Promise((y, n) => smtpTransport.sendMail(o, convert(y, n)));
return smtpTransport.sendMailAsync({
to: user.email,
from: 'email#school.edu',
subject: 'Password Reset',
text: `Hello etc etc`,
});
});
})
})
.then(() => res.status(200).send())
.catch(err => next(err));
}); // end POST route '/forgotPassword'