Correct Try...Catch Syntax Using Async/Await
Cleaner code
using async/await 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. The Promise .catch
is really no different from try/catch
.
Working harmoniously with await/async
, ES6 Promise's catch handler provides a proper solution and make code cleaner:
const createUser = await this.User
.create(userInfo)
.catch(error => console.error(error))
console.log(createdUser)
// business
// logic
// goes
// here
Note that while this answers the question, it gobbles up the error. The intention must for the execution to continue and not throw. In this case, it's usually always better to be explicit and return false from catch and check for user:
.catch(error => {
console.error(error);
return false
})
if (!createdUser) // stop operation
Likely, one will want to throw. So the complete answer as follows:
const createUser = await this.User
.create(userInfo)
.catch(error => {
// do what you need with the error
console.error(error)
// maybe send to Datadog or Sentry
// don't gobble up the error
throw error
})
console.log(createdUser)
// business
// logic
// goes
// here
Learning catch
doesn't seem like worth it?
The cleanliness benefits may not be apparent above, but it adds up in real-world complex async operations.
As an illustration, besides creating user (this.User.create
), we can push notification (this.pushNotification
) and send email (this.sendEmail
).
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)
.catch(error => {
// we can do some special error handling
// and throw back the error
})
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
})
Compose the operations:
const createdUser = await this.User
.create(userInfo)
.catch(error => {
// handle error
})
// business logic here
return await Promise.all([
this.pushNotification(userInfo),
this.sendEmail(userInfo)
]).catch(error => {
// handle errors caused
// by pushNotification or sendEmail
})
No try/catch. And it's clear what errors you are handling.
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 Handling multiple catches in promise chain 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.
Another simpler approach is to append .catch to the promise function. ex:
const createdUser = await this.User.create(userInfo).catch( error => {
// handle error
})