Is it safe to call a throwing function inside a catch block? - javascript

exports.createUser = functions.https.onCall(async (data, _context) => {
const email = data.email;
const password = data.password;
try {
// First, create the user account.
const userRecord = await admin.auth().createUser({
email: email,
password: password,
});
// User successfully created, now update the database.
const userId = userRecord.uid;
const db = admin.firestore();
const batch = db.batch();
batch.create(
db.collection("userAccounts").doc(userId), {created: admin.firestore.FieldValue.serverTimestamp()},
);
await batch.commit();
// Database successfully updated, now return the newly-created userId.
return Promise.resolve(userId);
} catch (error) {
if (userId) {
// There was an error updating the database. However, a user was created
// beforehand. Therefore, delete the user before terminating the
// function.
admin.auth().deleteUser(userId); // <-- this throws
throw new functions.https.HttpsError("unknown", "Database error", error);
}
throw new functions.https.HttpsError("unknown", "Authentication error", error);
}
});
This function performs two async tasks: creates a user and updates a database. If the database update fails, I need to undelete the user. This function deletes the user in the catch block when it catches a database error. However, deleting the user itself throws. If the delete-user function fails, would that error be caught in this catch block and possibly create an infinite loop? How best to handle this scenario?

It will not create an infinite loop, but the error won't be caught either - which should be avoided. You'll need to tack on another .catch, because you also want to throw a different error afterwards.
} catch (error) {
if (userId) {
// There was an error updating the database. However, a user was created
// beforehand. Therefore, delete the user before terminating the
// function.
admin.auth().deleteUser(userId)
.catch(() => {}); // Ignore errors thrown by deleteUser
throw new functions.https.HttpsError("unknown", "Database error", error);
}
throw new functions.https.HttpsError("unknown", "Authentication error", error);
}
You should also be sure that the caller of this callback passed to onCall catches its asynchronous errors to avoid unhandled rejections.
Still, this process is pretty suspect - it seems odd to be deliberately calling a function you think will probably throw. If you need to call deleteUser only in the case that createUser succeeds, checking for the userId looks like the right approach, but that'll only work if the variable is in scope at that point, which it isn't. You can change your code to:
exports.createUser = functions.https.onCall(async (data, _context) => {
const { email, password } = data;
let userId; // <---------------------------
try {
// First, create the user account.
const userRecord = await admin.auth().createUser({ email, password });
// User successfully created, now update the database.
userId = userRecord.uid; // <---------------------------
const db = admin.firestore();
const batch = db.batch();
batch.create(
db.collection("userAccounts").doc(userId), {created: admin.firestore.FieldValue.serverTimestamp()},
);
await batch.commit();
// Database successfully updated, now return the newly-created userId.
return userId; // don't need to wrap this in a Promise
} catch (error) {
if (userId) {
// There was an error updating the database. However, a user was created
// beforehand. Therefore, delete the user before terminating the
// function.
admin.auth().deleteUser(userId)
.catch(() => {}); // This SHOULDN'T throw, but just in case
throw new functions.https.HttpsError("unknown", "Database error", error);
}
throw new functions.https.HttpsError("unknown", "Authentication error", error);
}
});
Or to
exports.createUser = functions.https.onCall(async (data, _context) => {
const { email, password } = data;
let userId;
try {
// First, create the user account.
const userRecord = await admin.auth().createUser({ email, password });
userId = userRecord.uid;
} catch (error) {
throw new functions.https.HttpsError("unknown", "Authentication error", error);
}
try {
// User successfully created, now update the database.
const db = admin.firestore();
const batch = db.batch();
batch.create(
db.collection("userAccounts").doc(userId), { created: admin.firestore.FieldValue.serverTimestamp() },
);
await batch.commit();
// Database successfully updated, now return the newly-created userId.
return userId;
} catch (error) {
// There was an error updating the database. However, a user was created
// beforehand. Therefore, delete the user before terminating the
// function.
admin.auth().deleteUser(userId);
throw new functions.https.HttpsError("unknown", "Database error", error);
}
});

Related

Javascript: using catch block but not to handle an error

I'm in a situation where I have to use a catch block to execute some code but I don't want to consider it an error.
Basically, I want to update/create a user based on whether the user is already registered or not respectively. The admin sdk let me create a user, and if the user already exists it throws an error. So if I'm in the catch block I know that the user already exists and I want to update it.
function addClient(client) {
return new Promise((resolve, reject) => {
admin.auth().createUser({
uid: client.id,
email: client.email,
emailVerified: true,
password: client.password,
}).then(record => {
resolve(record);
return null;
}).catch(
// the user already exist, I update it
admin.auth().updateUser(client.id, {
email: client.email
}).then(record => {
resolve(record);
return null;
}).catch(
err => {
reject(err);
}
)
);
});
}
The problem is that when I call the function with an existing user, it is updated correctly but the HTTP response is an internal server error (I guess because it enters the catch block and it considers this as an error). The same is if I send a new user: it is created correctly but the HTTP response code is a 500.
There is a way to avoid this behaviour?
This is the main function that calls the previous one for each user received and it's responsible for sending the HTTP response:
exports.addClients = functions.https.onRequest((req, res) => {
// fetch recevied list from payload
var receivedClients = req.body.clients;
var promises = [];
receivedClients.forEach(client => {
promises.push(addClient(client));
})
Promise.all(promises)
.then(() => {
res.sendStatus(200);
return null;
})
.catch(err => {
res.status(500).send(err);
});
});
I guess that what I want to achieve is to have all the promises resolving.
You need to pass a callback to .catch, not a promise. Also avoid the Promise constructor antipattern!
function addClient(client) {
return admin.auth().createUser({
uid: client.id,
email: client.email,
emailVerified: true,
password: client.password,
}).catch(err => {
// ^^^^^^^^
// if (err.code != "UserExists") throw err;
return admin.auth().updateUser(client.id, {
email: client.email
})
});
}

Throwing errors in NodeJS - UnhandledPromiseRejection

I'm pretty new on NodeJS and I'm still trying to figure out how to handle errors. I read a lot of questions but I don't realize what I'm doing wrong.
Here I have a login function:
export const login = async (req, res) => {
let body = req.body;
try {
const user = await User.findOne({ username: body.username });
if (!user) {
throw new InternalError("Username or password are wrong");
}
if (!bcrypt.compareSync(body.password, user.password)) {
throw new InternalError("Username or password are wrong");
}
let token = jwt.sign({ data: user }, "secret", {
expiresIn: 60 * 60 * 24 * 30
});
return res.json({
user: user,
token: token
});
} catch (error) {
throw new GenericError(error);
}
};
And this is the error I get if, for example, I include a wrong password:
(node:12332) UnhandledPromiseRejectionWarning: GenericError: Username or password are wrong
(node:12332) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by
throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch()
I know the promises made with Await must have a .then() and .catch() but this is not an error on executing the promise but on validating the password.
What I want is to throw the error and get the response. Right now the request never ends and the previous error is displayed.
Thank you!
PS: InternalError and GenericError are just errors created by me which extends from Error
You receive the warning from node because you are re-throwing a GenericError after catching InternalError. Instead of re-throwing the GenericError, you should return your response or catch your GenericError when calling login.
Here is your code modified for both.
export const login = async (req, res) => {
// We get the body info
let body = req.body;
try {
// We find the user with the username from the body
const user = await User.findOne({ username: body.username });
// Let's assume no user exists, so we throw an InternalError,
// This skips straight to the catch block.
if (!user) {
throw new InternalError("Username or password are wrong");
}
// We never reach these two statements
// because of the error above being thrown.
if (!bcrypt.compareSync(body.password, user.password)) {
throw new InternalError("Username or password are wrong");
}
let token = jwt.sign({ data: user }, "secret", {
expiresIn: 60 * 60 * 24 * 30
});
res.json({
user: user,
token: token
});
} catch (err) {
// We caught our InternalError from not having a user above.
// Now we should return a response that the user is invalid.
// I am just going to return the error that we previously threw.
res.json({
error: err
});
}
};
You can definitely throw the GenericError from your catch block. However, doing that then requires you to catch the GenericError wherever you are calling your login function.
export const login = async (req, res) => {
// We get the body info
let body = req.body;
try {
// We find the user with the username from the body
const user = await User.findOne({ username: body.username });
// Let's assume no user exists, so we throw an InternalError,
// This skips straight to the catch block.
if (!user) {
throw new InternalError("Username or password are wrong");
}
// We never reach these two statements
// because of the error above being thrown.
if (!bcrypt.compareSync(body.password, user.password)) {
throw new InternalError("Username or password are wrong");
}
let token = jwt.sign({ data: user }, "secret", {
expiresIn: 60 * 60 * 24 * 30
});
res.json({
user: user,
token: token
});
} catch (err) {
// Throw the generic error
throw GenericError(err);
}
};
try {
// We call the login function that is defined above.
// It is going to throw a GenericError, so we have to
// catch it.
await login(req, res);
} catch (err) {
// Now we need to catch the generic error
res.json({
error: err
});
}

Express email error handling, for nested middleware

I have an express route for handling password resets, and with that i have a route, where i first find the user, and have some error handling with that, but now i want aditional error handling inside a nested function, and I'm not sure what pattern to use
function triggerPasswordResetEmailSend(req, res, next) {
var email = req.body.email;
if (!email) return res.status(422).json({error: "Please provide an email."});
UserRepositoryClass.findUserByEmail(email).then(user =>{
if(!user) return res.status(422).json({message: "User not found"})
sendPasswordReset(user);
return res.status(200).json({user: user});
}).catch(err =>{
return res.status(500).json({error: err})
});
}
Inside this function i do some initial error handling. The issue now is that the sendPasswordReset function can also throw errors, but there are not caught by the .catch() function, so I'm looking for something to handle this function.
I have tried passing the req and res objects into the function, but that does not seem like a good solution. I could do some try catch or maybe return a promise. But i want to ensure, that i follow the same pattern and best practises as i have already tried to do.
Here is the code snippet from my mail function:
module.exports = (user) => {
const userResetToken = generatePasswordToken();
UserRepositoryClass.setPasswordResetToken(user.id, userResetToken);
const passwordResetUrl = PASSWORD_RESET_URL(user._id, userResetToken);
return sendMail(options(user.email, passwordResetUrl));
}
You can use promise instead of function.
module.exports.sendPasswordReset = user = new Promise((resolve, reject) => {
const userResetToken = generatePasswordToken();
UserRepositoryClass.setPasswordResetToken(user.id, userResetToken);
const passwordResetUrl = PASSWORD_RESET_URL(user._id, userResetToken);
sendMail(options(user.email, passwordResetUrl))
.then(response => {
resolve(response, null); // we can get result as (data,error) here error is null
})
.catch(err => {
reject(null, err); // here response is null
});
});
You can use sendPasswordReset Promise like this:
sendPasswordReset(user).then((res, err) => {
// here you can get your res as well as err
if (err) throw new Error("Error while sending an email");
console.log("response", res);
});

Firebase Function Promise Chain not catching errors

I'm using a Firebase function that detects when a Stripe Token is added to a user collection, which then creates a Stripe User and creates a Subscription for that user.
The problem I'm having is that the below code has an error in it, and whilst I need to figure out what that error is, the Promise Chain doesn't seem to catch the actual error.
All that logs out is Function execution took 4375 ms, finished with status: 'connection error'
Does anyone know why this is happening. The only way I can get it to log catch the error is to nest the Promises.
exports.createStripeUser = functions.firestore
// A Stripe Token is created in a user, therefore they have agreed to pay
// Create a User in Stripe, and then attach the subscription to the Stripe customer
.document('users/{userId}/stripe/{stripeCollectionId}')
.onCreate((snap, context) => {
const stripeToken = snap.data().stripeToken;
let customer;
return admin.auth().getUser(`${context.params.userId}`)
.then(userObj => {
const user = userObj.toJSON();
return stripe.customers.create({
email: user.email,
source: stripeToken
})
})
.then(cust => {
customer = cust;
return db.collection('users').doc(context.params.userId).collection('plan').doc(context.params.userId).get()
})
.then(plan => {
return stripe.subscriptions.create({
customer: customer.id,
items: [{plan: plan.data().plan}]
})
})
.catch(e => {
console.log("ERRR", e)
throw new functions.https.HttpsError('unknown', e.message, e);
})
})
Wrap your block in a
try {
}
catch (e) {
// e holds the nested error message
}
block and pick up the error there.

Express.js and Bluebird - Handling the promise chain

In a backend API I have a login route which should perform the following sequence of actions:
Given an username and password, try to authenticate the user against an Active Directory. If authentication has failed reply with status 401. If success, continue.
Look for an user with the given username in the database. If not found reply with status 403, otherwise continue.
Find if the user document has some details like email, display name, etc (in case this is not the first time logging in). If yes reply with the user object, otherwise continue.
Get user details from the Active Directory and update the user object in the database. Reply with the updated object.
Code:
router.post('/login', (req, res, next) => {
// capture credentials
const username = req.body.username;
const password = req.body.password;
let user = null;
// authenticate
ad.authenticate(username, password)
.then((success) => {
if (!success) {
res.status(401).send(); // authentication failed
next();
}
return User.findOne({ username }).exec();
})
.then((found) => {
if (!found) {
res.status(403).send(); // unauthorized, no account in DB
next();
}
user = found;
if (user.displayName) {
res.status(201).json(user); // all good, return user details
next();
}
// fetch user details from the AD
return ad.getUserDetails(username, password);
})
.then((details) => {
// update user object with the response details and save
// ...
return user.save();
})
.then((update) => {
res.status(201).json(update); // all good, return user object
next();
})
.catch(err => next(err));
});
Now I had this running with callbacks but it was really nested. So I wanted to give Bluebird promises a try, but I have two problems:
Looks chaotic, any better way to chain the calls and handle responses?
Whenever I call next() to stop the request after replying, the execution continues to the other .then(). Although the client receives the correct response, in the server log I find that the execution have continued. For example, if there is no account in DB for a given user, the client receives the 403 response but in the server log I see an exception failed to read property displayName of null, because there was no user and it should have stopped in the next() after res.status(403).send();.
Best use if/else to make clear what branches will execute and which won't:
ad.authenticate(username, password).then((success) => {
if (!success) {
res.status(401).send(); // authentication failed
} else {
return User.findOne({ username }).exec().then(user => {
if (!user) {
res.status(403).send(); // unauthorized, no account in DB
} else if (user.displayName) {
res.status(201).json(user); // all good, return user details
} else {
// fetch user details from the AD
return ad.getUserDetails(username, password).then(details => {
// update user object with the response details and save
// ...
return user.save();
}).then(update => {
res.status(201).json(update); // all good, return user object
});
}
});
}
}).then(() => next(), err => next(err));
The nesting of then calls is quite necessary for conditional evaluation, you cannot chain them linearly and "break out" in the middle (other than by throwing exceptions, which is really ugly).
If you don't like all those then callbacks, you can use async/await syntax (possibly with a transpiler - or use Bluebird's Promise.coroutine to emulate it with generator syntax). Your whole code then becomes
router.post('/login', async (req, res, next) => {
try {
// authenticate
const success = await ad.authenticate(req.body.username, req.body.password);
if (!success) {
res.status(401).send(); // authentication failed
} else {
const user = await User.findOne({ username }).exec();
if (!user) {
res.status(403).send(); // unauthorized, no account in DB
} else if (user.displayName) {
res.status(201).json(user); // all good, return user details
} else {
// fetch user details from the AD
const details = await ad.getUserDetails(username, password);
// update user object with the response details and save
// ...
const update = await user.save();
res.status(201).json(update); // all good, return user object
}
}
next(); // let's hope this doesn't throw
} catch(err) {
next(err);
}
});
To answer your second point, you have to reject your promise after calling next() (or at least return something, otherwise the line after will be executed). Something like
next();
return Promise.reject()
and change your catch so it works if you do not have an error
.catch(err => {
if (err)
next(err)
});
To your second question first: there is no way to break/stop a promise chain, unless your callback throw err like
doAsync()
.then(()=>{
throw 'sth wrong'
})
.then(()=>{
// code here never runs
})
You can simply try below demos to verify the second callback still runs.
doAsync()
.then(()=>{
res.end('end')
})
.then(()=>{
// code here always runs
})
doAsync()
.then(()=>{
return;
})
.then(()=>{
// code here always runs
})
To your first question: to use the second parameter in then(), which means reject. And each time split the logic to two parts.
var p = new Promise(function(resolve, reject) {
return
ad.auth(username, password).then(()={
// check if 401 needed. If needed, return reject
if (dont needed 401 in your logic)
resolve(username)
else
reject({ msg: 'authentication has failed', status: 401 })
})
});
p
.then( (username)=>{
// this only runs when the previous resolves
return User.findOne({ username }).exec()
}, (data)=>{
// in fact in your case you dont even have to have the reject callback
return data
} )
.then( (found)=>{
return
new Promise(function(resolve, reject) {
if (found && /*your logic to determine it's not 403*/)
resolve(user)
else
reject({ msg: 'unauthorized, no account in DB', status: 403 })
})
} )
.then( (found)=>{
return
new Promise(function(resolve, reject) {
if (found && /*your logic to determine it's not 403*/)
resolve(user)
else
reject({ msg: 'unauthorized, no account in DB', status: 403 })
})
} )
.then( (user)=>{
return
new Promise(function(resolve, reject) {
if (/*your logic to determine it has the full info*/)
resolve(user)
else
return ad.getUserDetails(username, password)
})
} )
.then( (user)=>{
// all is good, do the good logic
}, (data)=>{
// something wrong, so here you can handle all the reject in one place
res.send(data)
} )

Categories