Error Handling Patterns in Async/Await

The simplest way to handle an error when using async/await is to wrap the lines using a await operator in a try block and handle the error on the subsequent catch block.

Looks like this, if you’re wondering:

1
2
3
4
5
6
7
(async () => {
try {
const movies = await getMovies({ studio: 'Pixar' })
} catch(err) {
// Handle errors
}
})()

The use of the try/catch statement with async/await makes sense: since the await operator allows us to write asynchronous code in a way it looks synchronous, we can also handle errors as if they were synchronous.

Inside the catch block we are free to put any code we want. It can be a function that rollbacks some database operation or just a console.log to print the error.

Note: I’ve seen many people put a single throw err inside of the catch block. That is redundant. If something goes bad on your function call it will just throw anyway (you don’t need to use try/catch).

Next sections will cover a bit of how we can structure our error handling code with async/await without just blindly wrapping things in a try/catch every time we use await. Spoiler: you don’t even have to use the try/catch if you want.

Outer Level try/catch

I will be using as example a simple Movie application scenario where we have a main function that calls two other helper functions.

First, we will write it taking a conservative approach and wrapping every occurrence of the await operator inside of a try/catch:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// helpers.js
export async function saveMovie(movie){
try{ /* Asynchronous movie logic */ }
catch(err){
// Handle errors
}
}
export async function saveActors(actors){
try{ /* Asynchronous actor logic */ }
catch(err){
// Handle errors
}
}
// main.js
import { saveActors, saveMovie } from './helpers'
const movie = { title : 'The Avengers 18: Kitten Rescue' }
const actors = [
'Robert Downey Jr.'
'Chris Evans',
'Kitten'
]
// Main IIFE function
(async () => {
try{
await Promise.all([
saveMovie(movie)
saveActors(actors)
])
}catch(err){
// Handle errors
}
})()

In the code above we are handling errors inside of the saveMovies and saveActors functions. A third try/catch error handler is also inside of our main function, which calls saveMovies and saveActors.

I did not specify how errors are handled inside of the catch blocks but here are two possible outcomes:

  • If the errors inside of saveMovies and saveActors are thrown, they will bubble up to the outer level catch in the main function;
  • If the errors inside of saveMovies and saveActors are “ignored” (eg. console.log(err)) then the outer level catch in the main function doesn’t have any use.

On this pattern I am interested on creating a “catch-all” error handler and reduce the unnecessary try/catch verbose on the code. To do that I can simply drop the first two try/catch blocks and handle the error only inside of the main function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// helpers.js
export async function saveMovie(movie){ /* Async movie logic */ }
export async function saveActors(actors){ /* Async actor logic */ }
// main.js
import { saveActors, saveMovie } from './helpers'
const movie = { /* Movie data */ }
const actors = [ /* Array of actors */ ]
(async () => {
try{
await Promise.all([
saveMovie(movie)
saveActors(actors)
])
}catch(err) {
// Errors from saveMovie and saveActors
// bubble up all the way to here and
// we can handle them in a single place
}
})()

Done! We can now handle the errors generically inside of the catch-all handler and use async/await in the help functions without worrying about errors.

The main idea here is to handle errors on the most outer level as possible.

One good thing about this way of handling errors is that you can still write a separate try/catch to handle specific errors. This opens doors to the next section:

Conditional Error Handling

A catch-all error handler doesn’t mean you can’t handle any specific errors when they happen. Any error handling code before the catch-all handler will intercept the error and you can choose between doing something or pass it to the catch-all handler.

To show it, let’s zoom in on the saveMovie function. I’ve added a call to a fake IMDB API. There we are interested on two types of errors: token expiration and “not found” HTTP errors:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// helpers.js
export async function saveMovie(movie){
// ...
let sequels = []
// Add a separate try/catch to intercept errors
try{
sequels = await imdbAPI.getSequels(movie)
}catch(err){
if(err.code === 'IMDB_TOKEN_EXPIRED'){
// Handle expired token by refreshing
// and retrying the operation
return refreshTokenAndRetry(actors)
} else if(err instanceof NotFoundError){
// No sequels were found, that is fine.
// Just log something on the console and
// the code outside the try/catch will continue
console.log(`No sequels found for ${movie.name}!`)
} else{
// Pass the unknown error to be handled
// at the "catch-all" error handler on the
// main function
throw err
}
}
// ...
}

JavaScript is not a typed language so there is no such thing as a typed catch. We need to either rely on instanceof checks or, depending on the error object, do some string comparison. The manual throw at the end doesn’t help it look better as well.

It gets the job done but, because of these reasons and all the nesting, this pattern can look like a workaround. Let’s see some alternatives.

Conditional Error Handling with Promise.catch

async/await is just some nice syntax sugar built on top of promises so we can always fallback to using Promise methods if we want. This means we are free to handle errors the same way we used to do with promises: by chaining the function call with one or more .catch() methods at the end.

Let’s put up some work on the last example and see how I can rewrite it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// helpers.js
function handleIMDBTokenExpiration(err){
if(err.code === 'IMDB_TOKEN_EXPIRED'){
// ...
}else{
throw err // Throws it to the next .catch() handler
}
}
function handleSequelNotFound(err){
if(err instanceof NotFoundError){
// ...
}else{
throw err // Throws it to the next .catch() handler
}
}
export async function saveMovie(movie){
// ...
// Chain .catch() methods at the end to handle errors
const sequels = await imdbAPI
.getSequels(movie)
.catch(handleIMDBTokenExpiration)
.catch(handleSequelNotFound)
.catch(err => {
// Leave the error to be handled
// by the catch-all error handler
throw err
})
// ...
}

There’s nothing new about this code, right? It’s just plain old promises. The only difference is that, instead of waiting for the value inside of then(), we paused the execution with await until the Promise resolves.

await-to-js

Link (npm)

Although this one is a npm package and not a language feature, I really think it deserves a place here.

The library is a wrapper function that leverages ES6 destructuring to make error handling similar to the way it is in Go. The code is very small, easy to understand, and you can replicate and adapt it to your codebase without much effort.

Let’s see how it works:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// helpers.js
import to from 'await-to-js'
export async function saveMovie(movie){
// ...
// Wrap the async call into to().
// First parameter is the error and second is the result.
// err === null means nothing went wrong
const [err, sequels] = await to(imdbAPI.getSequels(movie))
if(!err) {
console.log(`No errors! Keep things going`)
} else if(err.code === 'IMDB_TOKEN_EXPIRED') {
refreshTokenAndRetry(actors)
return
}else if(err instanceof NotFound) {
console.log(`No sequels found for ${movie.name}!`)
}else{
throw err
}
// ...
}

Looks like synchronous code, right?

Even tough the library is very opinionated on how you structure you code, it makes for a very clean solution. It reduces the nesting of using try/catch blocks and makes the code more readable.

Conclusion

This article explored a few different ways of handling errors besides the simple try/catch block and aims to extend upon the multiple error handling tutorials for async/await out there.

I’ll leave it up to you to decide what fits best on your code (and your mind). Also don’t stop here: you can combine some of these patterns together and create a different one. If you do so, let me know how it turned out!




By Ruan Martinelli