Handling failure and success in an array of asynchronous tasks
Handling failure and success in an array of asynchronous tasks whose execution may fail. TLDR; Promise.allSettled()
This is part of a series of posts where I refactor code from StackOverflow questions, with a discussion of the changes. One of the great things about JavaScript is how scalable it is. You can start with a simple script, and there is nothing wrong with that. Usually these posts are about refactorings other than what the questioner asked about, and would be out of scope for the SO answer. There is nothing wrong with code that runs, and “There is no such thing as good coding, only good refactoring”.
Here is the original code under discussion:
The questioner asks:
But I am not sure where do I need to put try-catch block. Wrapping each content function? Or inside my async function and for operator.
I’m going to give my answer verbatim from StackOverflow, and then a little extension to it.
Astute readers will recognise this pattern from “A Functional Refactor of Zeebe Node with fp-ts.
Answer
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:
You can chain a .catch()
handler on an async
function, and it has the same effect as wrapping it in try/catch
.
More in “A Functional Refactor of Zeebe Node with fp-ts, 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:
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:
Further discussion
A couple of things are missing from this.
- The ability to correlate the error and success results to the original task.
- Custom error-handling per task.
What gets populated into the eventual arrays of errors and results and just the outputs. You can answer the questions “How many tasks succeeded/failed and with what results/errors”, but you cannot answer the question “Which specific tasks failed?”
To do this, you would need either to pass a correlation through this machine, or report the result back to something listening to the task.
Application Architecture
How you do this really depends on where you plan to deal with the failure and success of the operations.
Now we start to move into the question of the structure of your application - the application architecture, the set of patterns and conventions that you use to assemble the logic of your application. And this is, in fact, at the crux of this question.
If you build a generalised machine for this class of operation (executing an array of asynchronous tasks that may succeed or fail), you cannot generalise specific operation error handling.
Do you want the caller to pass in an array of tasks, and then deal with the aggregate of success and failure?
Or do you want each task to be (at least optionally) responsible for dealing with it, and report the aggregate to the caller?
The easiest way to do it, would be to create an AsyncTask
class that has a run method, and require (or just allow) it to take a success
and failure
callback in its constructor. This allows the tasks themselves to handle their own failure and success if they want.
So, the AsyncTask
class looks something like this:
The executor now needs to deal with Promise branching, rather than just Promise chaining. The impact is in the runWithResult
function:
Actually doing this
I wouldn’t actually do this, but it is an interesting exercise.
You do need an application architecture - rather than custom-coding inline, refactoring the state machinery out to a first-class concern. The logic of your application is usually the custom part that actually produces unique business value.
However, rather than writing this state machinery, I would use something like fp-ts
, or in this specific case - I’d probably use Promise.allSettled
, a state machine that does exactly this, and is built in to JavaScript (supported from Node 12.9.0, Chrome 76, and Firefox 71). If you are using TypeScript, you need to target the es2020
lib.
It’s further evidence that state machines - when they are a means to an end in your application, rather than what your application does - belong is well-tested, sealed black boxes with a defined API that prevents them leaking their complexity into other areas of your application code.
About me: I’m a Developer Advocate at Camunda, working primarily on the Zeebe Workflow engine for Microservices Orchestration, and the maintainer of the Zeebe Node.js client. In my spare time, I build Magikcraft, a platform for programming with JavaScript in Minecraft.