Asynchronous Fishing: The Multi-Promise Resolution

December 11, 2016

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.

function allFulfilled(answerPs) {
	let countDown = answerPs.length;
	const answers = [];
	if (countDown === 0) { return answers; }
	const deferredResult = Q.defer();
	answerPs.forEach(function(answerP, index) {
		Q(answerP).when(function(answer) {
			answers[index] = answer;
			if (--countDown === 0) { deferredResult.resolve(answers); }
		}, function(err) {
			deferredResult.resolve(Q.reject(err));
		});
	});
	return deferredResult.promise;
}

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.

var a = () => Promise.resolve(1);
var b = () => Promise.reject(new Error(2));
var c = () => Promise.resolve(3);

Promise.all([a(), b(), c()].map(p => p.catch(e => e)))
	.then(results => console.log(results)) // 1,Error: 2,3
	.catch(e => console.log(e));

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

function xhr({method = 'GET', url, async = true, responseType = ''}){
	let req = new XMLHttpRequest();
	req.open(method, url, async);
	req.responseType = responseType;

	let p = new Promise((resolve, reject) => {
		req.onreadystatechange = () => {
			if (req.readyState == XMLHttpRequest.DONE){
				if (200 <= req.status && req.status < 300){
					resolve(req.response);
				} else {
					reject(req.response);
				}
			}
		}
		
		req.send();
	});
	
	return p;
}

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.

function all(promises){
	let len = promises.length,
		returned = 0,
		responses = [];

	let totalPromise = new Promise((resolve, reject) => {
		promises.forEach((p, i) => {	
			p.then(response => {
					responses[i] = response;
				})
				.catch(error => {
					responses[i] = new Error(error);
				})
				.then(() => {
					returned++;
					
					if (len == returned){
						resolve(responses)
					}
				});
		});
	});
		
	return totalPromise;
}

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.

function makeRequests(urls){
	let requests = urls.map(url => xhr({url: url, responseType: 'arraybuffer'}));
	
	all(requests)
		.then(response => {
			response.forEach(value => {
				if (value instanceof Error){
					console.error(value);
				}
				
				// Let's handle the value here.
			});
		});
}

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.