Asynchronous Fishing: The Multi-Promise Resolution
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.
The 404 - Fish not found.
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.
The Other Fish in the Sea
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:
Compared with $q.all()
and Q.all()
this 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.
The Graceful Catch
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.
The Easy Cast
For my implementation, I needed to return an ArrayBuffer of each file requested. A basic XMLHttpRequest can be mocked up in many different libraries but I decided I wanted to use strictly plain JavaScript. With the help of ES6’s destructured parameters, I was able to create a generic request with some default parameters that will return a Promise for me. Also, MDN always contains excellent documentation in Vanilla JS to build off when I’m in need of a reference.
NOTE: They’re still trying to make Fetch happen. - Mean Girls
The Multi-Promise Method
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 responses
array.
The Main Call
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 instanceOf
the Error
class. Now if I wanted total rejection to occur, I can throw
my Error
within the success function of the total Promise but I’m not forced to like with the other all
methods.
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.