Async/Await: Error Handling
Promises allowed us to catch the error by using an error handler in then
block or using a catch
block. Async/await has similar strategies.
Using catch with await
We await
an async
function (mostly, await
works with anything that produces thenable). Async
functions provide promises so we can still take advantage of a catch
block.
const myPromise = async () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject("We ran into an error");
}, 2000);
});
}
const main = async () => {
const response = await myPromise().catch((err) => {
console.log(err); // We ran into an error
})
}
main();
myPromise
gets rejected after 2 seconds with a message. When we are awaiting for this promise, we can chain a catch
block to handle the error.
Using catch when calling the function
We can also add the catch block when we are calling the async function.
const myPromise = async () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject("We ran into an error");
}, 2000);
});
}
const main = async () => {
const response = await myPromise();
}
main().catch((err) => {
console.log(err); // We ran into an error
});
Since main
is an async
function, it will return a promise so we can make use of a catch
block! Using catch
is smart, but it has a disadvantage. It will catch any error happening in the main
function and not only the error while awaiting myPromise
.
So if you had more code in the main
function that could result in an error, this catch
block would get that too.
Using a higher-order function
We can add such catch
blocks when calling the function but imagine if you have a large number of async function that you call in your application. Adding catch block to each one of them will become tiring. You do need to handle errors, though.
This is where a higher-order function will come into play. A higher-order function takes a function as an input and returns a function. These are used to transform the input function (in simple terms).
const myPromise = async () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject("We ran into an error");
}, 2000);
});
}
const main = async () => {
const response = await myPromise();
}
const handleError = (err) => {
console.log(err); // We ran into an error
}
const addingErrorHandler = (fn, errorHandler) => {
return function() {
fn().catch(errorHandler);
}
}
const mainFunctionWithErrorHandler = addingErrorHandler(main, handleError);
mainFunctionWithErrorHandler();
We have added three new things:
handleError
as a standard function to handle errors.addingErrorHandler
as a higher-order function. It takes a function and adds error handling to it.mainFunctionWithErrorHandler
converts themain
function to a function that can handle error using our higher-order function.
Adding all the functions might seem like a lot of code at the moment because we are doing this for one function main
. We will be able to reuse this error handler and higher-order function for x number of async functions in a large application.
Using a try/catch block
JavaScript provides us with try/catch block where you try a block of code and if an error occurs, catch it in the catch
block. We can use this with async/await as well.
const myPromise = async () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject("We ran into an error");
}, 2000);
});
}
const main = async () => {
try {
await myPromise(); // try this code
} catch (err) { // if it throws an error execute the catch block
console.log(err); // We ran into an error
}
}
main();
Try/catch is the easiest to technique to understand and implement. It is excellent in a not so complex application, but I do prefer higher-order functions as the application becomes bigger.