Fishing was an analogy that came to mind when I tried to explain a problem I was having to a non-developer. The notion that an HTTP request is like casting a line out and waiting for a bite or response was what I came up with. If I caught a fish, it was a 200 status code, otherwise a missed attempt errored out. Finding an analogous event comparable to an error or bad response was slightly more difficult and not worth the additional effort for the explanation. You understand the point. Now to the problem I was having and my “deep dive” in understanding various solutions.
A project I was working on required that I process several files requested from the server, but I couldn’t run this process until I received them all. As if I was out at sea on a fishing vessel and had several lines casted out and I couldn’t pack up and head home until I reeled them all in.
With ES6 Promises, there is the
Promise.all method that takes an Array (or any iterable) of Promises as a parameter and returns a Promise that resolves with an array of all the individual Promises’ resolutions. But from my understanding, if one of the Promises were to be rejected, the entire resulting Promise would be rejected. I like to call this “total rejection.” This was not going to work for me. If I didn’t get a snag on one of the lines, I still wanted the fish from my other lines. Naturally, I decided to do some digging and construct my own way of doing a multi-promise resolution.
Working extensively with AngularJS (1.x), I decided to see how the
$q.all() function handles it. In their documentation, they mention that
$q “is a Promises/A+-compliant implementation of promises/deferred objects inspired by Kris Kowal’s Q.” Now knowing this, I was compelled to find one of the earliest constructions of this method that I could. Within the source of Kowal’s Q, he mentions that the
Q.all() method was created by Mark Miller. So once more down the rabbit hole, I found a seemingly original implementation of the multi-promise resolution.
Let’s take a look at how Miller implemented this technique:
allFulfilled method is theoretically the same concept.
Not to my surprise, you can see how he went with the notion of total rejection within his for loop. If the
Q(answerP).when() executes the error callback, the whole
deferredResult promise resolves as rejected. I needed to find another way, so I kept diving.
Although I felt it could be more expressive, I found this StackOverflow inspiration to be a good thrust in the right direction towards a solution.
I figured that catching these errors and returning them within the array of results was the best course for me to take. Some library implementations have this already. For example, when.js has a
settle method that returns an array of objects that contain a pass-fail-like state property. But I felt I could come up with something a bit more intuitive.
Once I had that simple request method setup, I decided to create my multi-promise
all method. Now I would like to add this to the Promise class simply for ease of use, but I prefer to never overwrite an object I don’t own. For sake of this example I am just going to leave it as a general method.
As you can see, like Mark Miller’s
allFulfilled implementation, I loop through the Promises and add the response to an array. But if I receive an error, my
catch callback will continue to add it to the array but instantiating a new Error to maintain the stack trace as well as easily identifying that as an error later on.
Since the asynchronous requests can return a response at varying times, the total Promise may resolve in a different order than the array of Promises that was passed in. To maintain the integrity of my output array, I add my responses in the index position of the
promises array or the order corresponding with which they were requested and not simply pushed onto the end of the
Wrapping it all together is now as simple as creating request Promises, adding them to an array, and making my
all method call on them. This total Promise will resolve with my array of responses, which can each be handled as needed.
Detecting if any errors occurred only requires a simple check to see if the value is the
Error class. Now if I wanted total rejection to occur, I can
Error within the success function of the total Promise but I’m not forced to like with the other
Now depending on the circumstances of the initial Promises array, there may be alterations necessary to fit the specific use case. I found this solution to work well in my situation but it may not be all encompassing. I’m curious to see other ways in which this problem has been solved.