⏲ Async/Await and handling errors
- Published on
The Promise
async/await
AKA how to write async code in 2021, was a result of callback hell and nested promises. What are those? Well take a look at the examples:
Promises, well...promised this:
doSomeFetch()
.then((data) => doAnotherFetch(data))
.then((result) => doAnoterFetch(result))
.then((outcome) => probablyLastFetch(outcome))
Although, a real world use-case turned out like this:
doSomeFetch().then((data) => {
doAnotherFetch(data).then((result) => {
doAnoterFetch(data, result).then((outcome) => {
probablyLastFetch(data, result, outcome).then((fin) => {
// Ah Finally!
})
})
})
})
And don't even get me started on error handling:
doSomeFetch().then((data) => {
doAnotherFetch(data)
.then((result) => {
doAnoterFetch(data, result)
.then((outcome) => {
probablyLastFetch(data, result, outcome)
.then((fin) => {
// Ah Finally!
})
.catch((e) => {
handleError(e)
})
})
.catch((e) => {
handleError(e)
})
})
.catch((e) => {
handleError(e)
})
})
asnyc/await
allows us to write code like this:
const data = await doSomeFetch()
const result = await doAnotherFetch(data)
const outcome = await doAnoterFetch(data, result)
const fin = await probablyLastFetch(data, result, outcome)
Errors..to catch or throw?
Looks cool right? Now how about we introduce some error handling:
let data, result, outcome, fin
try {
data = await doSomeFetch()
} catch (e) {
handleError(e)
}
try {
result = await doAnotherFetch(data)
} catch (e) {
handleError(e)
}
try {
outcome = await doAnoterFetch(data, result)
} catch (e) {
handleError(e)
}
try {
fin = await probablyLastFetch(data, result, outcome)
} catch (e) {
handleError(e)
}
An inquisitive mind like yours would ask, why not use a single try/catch
and wrap all fetch call? That's a valid suggestion, however it depends on whether you need the catch
to do something different depending on which function caused the error. A single try/catch
would be something like this:
try {
const data = await doSomeFetch()
const result = await doAnotherFetch(data)
const outcome = await doAnoterFetch(data, result)
const fin = await probablyLastFetch(data, result, outcome)
} catch (e) {
handleError(e)
}
If we want to be able to catch
and identify which block threw an error and handle each case differently, we append a catch
block to each async
method, kind of similar to the promises way:
const data = await doSomeFetch().catch(handleError)
const result = await doAnotherFetch(data).catch(handleError)
const outcome = await doAnoterFetch(data, result).catch(handleError)
const fin = await probablyLastFetch(data, result, outcome).catch(handleError)
Nice right? But now each catch method has it's own context, and it's kinda difficult to use things decalared inside the catch method outside of it's context.
A Clean Approach
Building up on our previous examples, let's create a better error handler:
async function fancyFetch(fetcher, ...args) {
try {
const data = await fetcher(...args)
return [data, null]
} catch (error) {
log(error)
return [null, error]
}
}
If we have data, we return an array with data as the first parameter and error as null, if we get and error, we return data as null and the error object.
Now to re-write up the async/await
example using an array destructring syntax:
const [data, dataError] = await fancyFetch(doSomeFetch);
if(dataError) { // got the error context, do something! }
const [result, resultError] = await fancyFetch(doAnotherFetch, data);
if(resultError) { // got the error context, do something! }
const [outcome, outcomeError] = await fancyFetch(doAnotherFetch, data, result);
if(outcomeError) { // got the error context, do something! }
const [fin, finError] = await fancyFetch(probablyLastFetch, data, result, outcome);
if(finError) { // got the error context, do something! }
This feels much more cleaner in terms of readability. We even can add logging methods to the catch
block which talks to some analytics service. As an added advantage, the codebase now has a consistent error handling mechanism. The fancyFetch
method here is a very high level abstract. In the real world use-case, you'd want to add validations for the fetcher
method and better logs in the catch
block.
Takeaways
-
If you don't care about handling errors for individual async calls, go with a single
try/catch
. -
If you care about handling errors for individual async calls, but do not care about the things inside the context of the catch block, use
await doSomeFetch().catch(handleError);
-
If the answer for both of the above points were 'No', then the
fancyFetch()
approach should work for you.