I have an async function where there is some operations like this:
async function myAsyncFunction(argument) {
let arrayTasks = [
{
const run = async () => {
let response = await getDataAsync();
}
},
{
const run = async () => {
let response = await getDataAsync();
}
}
]
for (const index in arrayTasks) {
await arrayTasks[index].run();
}
}
The function above is a simplified version of my code, it works. But I am not sure where do I need to put try-catch block:
Wrapping all content function:
async function myAsyncFunction(argument) {
try{
// All code
}catch (e) {
// catch code
}
}
Or inside my asyncs functions and for operator:
async function myAsyncFunction(argument) {
let arrayTasks = [
{
const run = async () => {
try{
let response = await getDataAsync();
}catch (e) {
// catch code
}
}
},
{
const run = async () => {
try{
let response = await getDataAsync();
}catch (e) {
// catch code
}
}
}
]
for (const index in arrayTasks) {
try{
await arrayTasks[index].run();
}catch (e) {
// catch code
}
}
}
What is a correct way?
The arrayTasks variable is dynamic length on my original code.
Depends how and where you want to handle failure.
One approach to "an array of asynchronous tasks whose execution may fail" is a pattern like this:
async function myAsyncFunction(argument) {
const success = async res => ({ success: await res }) // Promise<{success: res}>
const error = async err => ({ error: await err }) // Promise<{error: e}>
const arrayTasks = [
{
run: async () => getDataAsync()
}
},
{
run: async () => getDataAsync()
}
]
const runWithResult = task => task.run().then(success).catch(error)
const outcomes = await Promise.all(arrayTasks.map(runWithResult))
console.log(outcomes) // array of {error: e} | {success: res}
}
You can chain a .catch() handler on an async function, and it has the same effect as wrapping it in try/catch.
More here, if you are interested. The section "Refactor without fp-ts" shows how to reduce that array from [{error: e} | {success: res}] to {error: [], success: []}, which is way easier to work with:
const { error, success } = outcomes.reduce((acc, o) => o.error ?
{ error: [...acc.error, o.error], success: acc.success } :
{ error: acc.error, success: [...acc.success, o.success] },
{ error: [], success: [] })
This is an FP type called Either - an operation may return "either" (in this case) a value or an error.
Your code doesn't throw with this approach.
If you know that something may fail, it's not exceptional. Exceptions are when some unexpected failure occurs, IMHO.
"Tasks that may fail" that are known ahead of time just need the error path code written.
If you take this approach, I recommend building it as a first-class state reducing machine, like this:
// Takes an array of async tasks that may throw of shape {run: () => Promise<result>}
// Returns Promise<{error: error[], success: result[]}>
async function executeAsyncTasks(arrayOfAsyncTasks) {
const success = async res => ({ success: await res }) // Promise<{success: res}>
const error = async err => ({ error: await err }) // Promise<{error: e}>
const runWithResult = task => task.run().then(success).catch(error)
const outcomes = await Promise.all(arrayOfAsyncTasks.map(runWithResult))
const outcomesFlat = outcomes.reduce((acc, o) => o.error ?
{ error: [...acc.error, o.error], success: acc.success } :
{ error: acc.error, success: [...acc.success, o.success] },
{ error: [], success: [] })
return outcomesFlat
}
It all depends... My rule of thumb is only catch something if you intend on doing something with it, i.e some error handling. If the handling of the error is going to be the same across all cases, wrap the it all with one try/catch, else wrap individual cases, with their own error handling code.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With