Correct Try...Catch Syntax Using Async/Await
JavascriptPromiseAsync AwaitTry CatchEcmascript 2017Javascript Problem Overview
I like the flatness of the new Async/Await
feature available in Typescript, etc. However, I'm not sure I like the fact that I have to declare the variable I'm await
ing on the outside of a try...catch
block in order to use it later. Like so:
let createdUser
try {
createdUser = await this.User.create(userInfo)
} catch (error) {
console.error(error)
}
console.log(createdUser)
// business
// logic
// goes
// here
Please correct me if I'm wrong, but it seems to be best practice not to place multiple lines of business logic in the try
body, so I'm left only with the alternative of declaring createdUser
outside the block, assigning it in the block, and then using it after.
What is best practice in this instance?
Javascript Solutions
Solution 1 - Javascript
> It seems to be best practice not to place multiple lines of business logic in the try body
Actually I'd say it is. You usually want to catch
all exceptions from working with the value:
try {
const createdUser = await this.User.create(userInfo);
console.log(createdUser)
// business logic goes here
} catch (error) {
console.error(error) // from creation or business logic
}
If you want to catch and handle errors only from the promise, you have three choices:
-
Declare the variable outside, and branch depending on whether there was an exception or not. That can take various forms, like
- assign a default value to the variable in the
catch
block return
early or re-throw
an exception from thecatch
block- set a flag whether the
catch
block caught an exception, and test for it in anif
condition - test for the value of the variable to have been assigned
let createdUser; // or use `var` inside the block try { createdUser = await this.User.create(userInfo); } catch (error) { console.error(error) // from creation } if (createdUser) { // user was successfully created console.log(createdUser) // business logic goes here }
- assign a default value to the variable in the
-
Test the caught exception for its type, and handle or rethrow it based on that.
try { const createdUser = await this.User.create(userInfo); // user was successfully created console.log(createdUser) // business logic goes here } catch (error) { if (error instanceof CreationError) { console.error(error) // from creation } else { throw error; } }
Unfortunately, standard JavaScript (still) doesn't have syntax support for conditional exceptions.
If your method doesn't return promises that are rejected with specific enough errors, you can do that yourself by re-throwing something more appropriate in a
.catch()
handler:try { const createdUser = await this.User.create(userInfo).catch(err => { throw new CreationError(err.message, {code: "USER_CREATE"}); }); … } …
See also https://stackoverflow.com/q/26076511/1048572 for the pre-
async
/await
version of this. -
Use
then
with two callbacks instead oftry
/catch
. This really is the least ugly way and my personal recommendation also for its simplicity and correctness, not relying on tagged errors or looks of the result value to distinguish between fulfillment and rejection of the promise:await this.User.create(userInfo).then(createdUser => { // user was successfully created console.log(createdUser) // business logic goes here }, error => { console.error(error) // from creation });
Of course it comes with the drawback of introducing callback functions, meaning you cannot as easily
break
/continue
loops or do earlyreturn
s from the outer function.
Solution 2 - Javascript
Another simpler approach is to append .catch to the promise function. ex:
const createdUser = await this.User.create(userInfo).catch( error => {
// handle error
})
Solution 3 - Javascript
I usually use the Promise's catch()
function to return an object with an error
property on failure.
For example, in your case i'd do:
const createdUser = await this.User.create(userInfo)
.catch(error => { error }); // <--- the added catch
if (Object(createdUser).error) {
console.error(error)
}
If you don't like to keep adding the catch()
calls, you can add a helper function to the Function's prototype:
Function.prototype.withCatcher = function withCatcher() {
const result = this.apply(this, arguments);
if (!Object(result).catch) {
throw `${this.name}() must return a Promise when using withCatcher()`;
}
return result.catch(error => ({ error }));
};
And now you'll be able to do:
const createdUser = await this.User.create.withCatcher(userInfo);
if (Object(createdUser).error) {
console.error(createdUser.error);
}
EDIT 03/2020
You can also add a default "catch to an error object" function to the Promise
object like so:
Promise.prototype.catchToObj = function catchToObj() {
return this.catch(error => ({ error }));
};
And then use it as follows:
const createdUser = await this.User.create(userInfo).catchToObj();
if (createdUser && createdUser.error) {
console.error(createdUser.error);
}
Solution 4 - Javascript
@Bergi Answer is good, but I think it's not the best way because you have to go back to the old then() method, so i think a better way is to catch the error in the async function
async function someAsyncFunction(){
const createdUser = await this.User.create(userInfo);
console.log(createdUser)
}
someAsyncFunction().catch(console.log);
- But what if we have many
await
in the same function and need to catch every error?
You may declare the to()
function
function to(promise) {
return promise.then(data => {
return [null, data];
})
.catch(err => [err]);
}
And then
async function someAsyncFunction(){
let err, createdUser, anotherUser;
[err, createdUser] = await to(this.User.create(userInfo));
if (err) console.log(`Error is ${err}`);
else console.log(`createdUser is ${createdUser}`);
[err, anotherUser] = await to(this.User.create(anotherUserInfo));
if (err) console.log(`Error is ${err}`);
else console.log(`anotherUser is ${anotherUser}`);
}
someAsyncFunction();
> When reading this its: "Wait to this.User.create".
Finally you can create the module "to.js" or simply use the await-to-js module.
You can get more information about to
function in this post
Solution 5 - Javascript
Cleaner code
for async/await error handling with Promise catch handler.
From what I see, this has been a long-standing problem that has bugged (both meanings) many programmers and their code.
ES6 Promise's catch handler provides a proper solution:
this.User.create(userInfo).then(createdUser => {
console.log(createdUser)
// business
// logic
// goes
// here
}).catch(err => {
//handle the error
})
But that looks like a removal of async/await altogether. Not true.
The idea is to use Promise style and catch for top level caller. Otherwise, continue to use async/await.
Example, besides creating user (this.User.create
), we can push notification (this.pushNotification
) and send email (this.sendEmail
). All are async operations. There are no catch handlers required. Just async/await.
this.User.create
this.User.create = async(userInfo) => {
// collect some fb data and do some background check in parallel
const facebookDetails = await retrieveFacebookAsync(userInfo.email)
const backgroundCheck = await backgroundCheckAsync(userInfo.passportID)
if (backgroundCheck.pass !== true) throw Error('Background check failed')
// now we can insert everything
const createdUser = await Database.insert({ ...userInfo, ...facebookDetails })
return createdUser
}
this.pushNotifcation and this.sendEmail
this.pushNotification = async(userInfo) => {
const pushed = await PushNotificationProvider.send(userInfo)
return pushed
})
this.sendEmail = async(userInfo) => {
const sent = await mail({ to: userInfo.email, message: 'Welcome' })
return sent
})
When all the async operations are composed together, we can use Promise-style with a #catch handler attached:
this.User.create(userInfo).then(createdUser => {
console.log(createdUser)
// business logic here
return Promise.all([
this.pushNotification(userInfo),
this.sendEmail(userInfo)
])
}).catch(err => {
// handle err
})
If we were to do this with try/catch, one will have to wrap everything in try/catch (not advisable), or set up many try/catches:
var createdUser
try {
createdUser = await this.User.create(userInfo)
} catch (err) {
//handle err
}
console.log(createdUser)
// business logic here
if (createdUser) {
try {
await this.pushNotification(userInfo)
await this.sendEmail(userInfo)
} catch (err) {
// handle err
}
}