Execute promises when all resolved (as DB Transactions) - javascript

I have two promises that need to act as Transactions, if one of them fails, a "rollback" should occur.
For example: When I create a user I want to send a confirmation email, but what happens if there is an error on sending the email, the user will be created but will never get a confirmation.
await saveUser(user)
await sendEmail(user)
If I switch the order, what happens when something goes wrong on creating the user? It will get a confirmation.
await sendEmail(user)
await saveUser(user)
I thought a Promise.all will do the trick, but it doesn't.
await Promise.all([saveUser(user), sendEmail(user)]);
Is there a way to execute promises as transactions on Javascript? Promises should execute if and only if both of them resolve.

There is no built in 'transaction mechanism' in JavaScript like there is in some databases. You cannot go back in time to undo operations which you have already performed. If you wish to reverse the effects of saveUser when sendEmail fails, then you need to create another function, such as unsaveUser, and invoke it if sendEmail fails.
This can be done in the following way:
Using Promises:
saveUser(user)
.then(() => sendEmail(user))
.catch(err => {
if (err.message === "Email Error") { // Check the exception to determine if the email failed
unsaveUser(user);
}
});
// This will only send the email if 'saveUser' was successful, and call 'unsaveUser' if email fails
Same thing using Async/Await:
try {
await saveUser(user);
sendEmail(user);
} catch(err) {
if (err.message === "Email Error") { // Check the exception to determine if the email failed
unsaveUser(user);
}
}
For this example to work, in your async sendEmail function, you must throw new Error("Email Error"); upon detecting an error. If you are using resolve/reject, it would be reject(new Error("Email Error"));.
I hope this is clear to you and helps you resolve your issue.

Related

How do I catch specific error returned by Firebase Auth, when user updates their password?

I am writing an app, where I want to give the user possibility to change their password. So I have a simple UpdatePassword.js page, where I invoke Firebase Authentication .updatePassword(password) method. As explained in the docs, this is a sensitive operation, and as such, the user needs to authenticate (if they haven't authenticated recently), in order to change their password to a new one.
This is my method:
const update = async () => {
const user = await firebase.auth().currentUser;
await user
.updatePassword(password)
.then(() => {
setUpdated(true);
})
.catch((error) => {
//I want to handle this specific error but I don't know how
if (
error.message ===
"This operation is sensitive and requires recent authentication. Log in again before retrying this request."
) {
console.log("should display a modal for user to authenticate again");
}
console.log("error while updating pass: ", error);
setSaving(false);
});
};
As you can see from my console.logs, in the case where the user needs to authenticate again, I want to display a modal, where they will sign in with their credentials again. This is not a problem and is easy to do. However, my question is, how do I catch this specific type of error where the user needs to authenticate? As per my console.logs, the way I have implemented it right now, I am just comparing the error message which I receive from Firebase Authentication, which is really not the right way to do. What if Firebase Auth change the error message to something else? Is there something like an error code which I can compare to the error thrown, and handle the exception by error code or something more safe than just a string message?
As you will see in the doc, the error that is thrown in this case (i.e. "if the user's last sign-in time does not meet the security threshold") has an auth/requires-recent-login error code.
So:
//...
.catch((error) => {
if (error.code === 'auth/requires-recent-login') {
// Display the modal
} else {
// ...

Any suggestions on how to implement challenge (2 factor authentication, etc...) in authentication

I am trying to incorporate 2FA in the react admin login flow.
The issue is that the standard way to validate a login is to use useLogin.
const login = useLogin();
try {
await login({username: "joeblack", password: "mybadpassword"}, "/redirectlocation");
} catch (err) {
// display an error notice or whatever
}
Basically, the login function from useLogin will either complete the login process and log the user in or show an error.
Second Authentication Step
For things like 2FA, new password required, etc..., there needs to be an in between step where the user isn't authenticated yet to view resources, but is not in an error state.
So for instance, perhaps login would return a challenge with the type of challenge.
Technically this can be done by returning that info in the authProvider login function and then making decisions based on that.
const loginResult = login({username: "joeblack", password: "mybadpassword"});
// loginResult returns { challenge: "2FA" }
if (challenge) {
// redirect to challenge page
} else {
// redirect to dashboard or wherever
}
The issue is that even if we handle it after the login function, once that login function runs, technically the user is authenticated. So they could just bypass the challenge and directly input the resource they want and they would be able to view it.
The login function of the authProvider only has 2 results, a resolved promise or rejected promise.
Before I go further to figure out how to make this work I wanted to see if anyone else has looked into this issue.
This is basically a workaround, but I think it's the best option for now (although I'd love to be proven wrong).
The idea is that even though Amplify's auth comes back with a resolved promise containing the Cognito user, we check if there is a challenge property. If there is, then we reject the login promise so react-admin doesn't log the user in.
Then we return an error message in the rejected promise that can be read by whatever is calling the login function. If it's a challenge then the login interface can handle it appropriately presenting a 2FA screen to confirm.
Had to also include the Cognito user in the our case because we need that in order to confirm the 2FA.
Here is the authProvider login function
login: async ({ username, password }) => {
try {
const cognitoUser = await Auth.signIn(username, password);
if (cognitoUser.hasOwnProperty("challengeName")) {
return Promise.reject({
message: cognitoUser.challengeName,
cognitoUser: cognitoUser,
});
} else {
return { ...createUser(cognitoUser) };
}
} catch (err) {
return Promise.reject({ message: err.code });
}
}
Hopefully that helps someone. Let me know if you have a better solution.

Can We Explicitly Catch Puppeteer (Chrome/Chromium) Error net::ERR_ABORTED?

Can we explicitly and specifically catch Puppeteer (Chromme/Chromium) error net::ERR_ABORTED? Or is string matching the only option currently?
page.goto(oneClickAuthPage).catch(e => {
if (e.message.includes('net::ERR_ABORTED')) {}
})
/* "net::ERROR_ABORTED" occurs for sub-resources on a page if we navigate
* away too quickly. I'm specifically awaiting a 302 response for successful
* login and then immediately navigating to the auth-protected page.
*/
await page.waitForResponse(res => res.url() === href && res.status() === 302)
page.goto(originalRequestPage)
Ideally, this would be similar to a potential event we could catch with page.on('requestaborted')
I'd recommend putting your api calls and so in a trycatch block
If it fails, you catch the error, like you are currently doing. But it just looks a bit nicer
try {
await page.goto(PAGE)
} catch(error) {
console.log(error) or console.error(error)
//do specific functionality based on error codes
if(error.status === 300) {
//I don't know what app you are building this in
//But if it's in React, here you could do
//setState to display error messages and so forth
setError('Action aborted')
//if it's in an express app, you can respond with your own data
res.send({error: 'Action aborted'})
}
}
If there are not specific error codes in the error responses for when Puppeteer is aborted, it means that Puppeteer's API has not been coded to return data like that, unfortunately :')
It's not too uncommon to do error messages checks like you are doing in your question. It's, unfortunately, the only way we can do it, since this is what we're given to work with :'P

Flow control with async without throwing Errors

I am having an issue where Intellij is warning me about 'throw' of exception caught locally. After doing some digging on why this is not ok it makes sense, errors should not be used for flow control. The problem I am facing though is in async I cannot reject the promise without throw something locally and I get the warning.
Some of my example code.
Top level takes in the request and awaits for the response from the controller:
Router.post("/", async (req, res) => {
try {
let itemController = new ItemController(req.body);
let response = await itemController.request();
res.send(response);
} catch (error) {
res.status(500).send({ error: error});
}
});
The controller takes in the request and awaits on other functions to get some data.
async request() {
try {
await isValidItem();
return await this.initialize();
} catch(error) {
throw error;
}
}
Then I have a function which gets manufacturer ID for this item and I run into my problem. If the SQL query doesn't fail and nothing is in the response I need to throw a local error so that the request can fail gracefully. and send a proper 500 error to the client.
async queryManufacturerID() {
try {
let result = await this.queryManufacturerID(this.itemID, this.brand);
if (result === false) {
throw new Error("this item has no manufacturer ID");
} else {
this.manufacturerID = result["manufacturerItemID"];
}
} catch (error) {
throw error;
}
}
My problem is I know I can adjust this so other functions that get a reply from this can know that this function failed without a error but that would have to happen in this entire chain to prevent throwing locally. Seems like a lot of bloat.
The only thing that makes this code bloaty and the IDE complain is not throwing errors, but this:
try {
//...
} catch (error) {
throw error;
}
It's a no op. You can safely remove it without changing the logic.
The only case where you should use catch is when you actually plan to handle the error and get the execution back on track.
After doing some digging on why this is not ok it makes sense, errors should not be used for flow control
I disagree. Errors are a part of proper flow control, they allow you to handle unplanned things. Throwing an error if something unexpected occured makes sense, also in this case.

Using express, await, catch and next to stop function execution on error

I'm writing an express JS app using this style for routing:
router.post('/account/create', async function(req, res, next) {
var account = await db.query(`query to see if account exists`).catch(next);
if (account) {
res.send('Email is unavailable.');
} else {
// Create account
}
});
If the query returns successful but with no rows, the route executes perfectly. account is empty and so the if statement works and we create an account.
However if there was an issue with the db query, the catch statement is called and account is undefined, so the function continues to attempt to create a new account, even though next has been called which logs the error and sends a 500.
In an effort to continue with the ease of this async/await simple coding style, is there a way to easily stop function execution (or another solution) to prevent the subsequent code from executing without going back to callbacks?
Something like below should do the job?
It utilises try / catch, coupled with async/await, this way there are no callbacks.
router.post('/account/create', async function(req, res, next) {
var account;
try {
account = await db.query(`query to see if account exists`);
} catch (e) {
return res.status(500).send("Error checking if account exists.");
}
// If the account exists, return early
if (account) {
return res.status(500).send("Account already exists.");
}
// Account doesn't exist, so let's create the account!
try {
// Here you could create your new user account, and save it in the database, the catch would catch any DB error.
// await db.query......
} catch (e) {
// Something went wrong creating the account, oops! Return.
return res.status(500).send("Error creating account");
}
// The account would have been created at this point.
return res.status(200).send("Account created!");
});
Using promises, without async/await.
router.post('/account/create', async function(req, res, next) {
db.query(`query to see if account exists`)
.then((account) => {
// If the account exists, return early
if (account) {
return res.status(500).send("Account already exists.");
}
// Now create account
db.query(`query to create account`)
.then((result) => {
// Account created fine, return.
return res.status(200).send("Account created!");
})
.catch((err) => {
// Error creating account, return.
return res.status(500).send("Error creating account");
});
})
.catch((err) => {
return res.status(500).send("Error checking if account exists.");
})
});
I've decided to use the solution here which is to wrap my route handlers in a function that catches errors for the entire route handler and calls next. Then if I need to handle an error specifically I can use try-catch. So 90% of cases use the default next error handler, the other 10 just use try-catch.
Keeps everything clean and super convenient, and you don't ever have to use .catch() on await calls.

Categories

Resources