in Appcelerator Titanium, Mobile

Handling Multiple JavaScript Promises Even if Some Fail

javascript-promises

I was recently asked a question related to my post about using Promises in Appcelerator apps that I thought would be useful to share. The person asking the question has an app that makes multiple API requests to fetch data needed for a particular function of the app. The API requests are fulfilled with JavaScript promises. If one or more of these requests may fail, the corresponding promise(s) are of course rejected.

They are using Promise.all() to track when all the API requests complete so they can do something with the returned data, but if one or more of the API requests fails or times out, then the Promise.all is rejected as well.

So the question is: how do we track and use the data from the API requests that do succeed?

Even if some of the requests fail, they still want to be able to do something with the API requests that did return data. So the question is: how do we track and use the data from the API requests that do succeed? In other words, make it so the Promise.all isn’t rejected when one or more of the API requests are rejected.

An Example

Consider 3 API requests that return promises. We’ll track the completion of them with Promise.all()

var p1 = new Promise(function (resolve, reject) {
    resolve('one');
});

var p2 = new Promise(function (resolve, reject) {
    reject('two was rejected');
});

var p3 = new Promise(function (resolve, reject) {
    resolve('three');
});

Promise.all([p1, p2, p3])
.then(function(res){
    console.log('Promise.all', res);
})
.catch(function(err){
    console.error('Promise.all error', err); 
});

//Promise.all error two was rejected

Even though p1 and p3 resolve, the Promise.all() will fail and log an error of Promise.all error two was rejected because p3 is getting rejected. This is expected behavior, but in our case, we still want to use the data that was returned in the other promises.

Fix

The fix is pretty simple: we can catch() the rejected promise at the request.

...
var p2 = new Promise(function (resolve, reject) {
    reject('two was rejected');
})
.catch(function(err){
    console.error('err', err);
});
...

//err two was rejected
//Promise.all [ 'one', undefined, 'three' ]

Now our Promise.all() successfully resolves. You can see that p1 and p3 have return values, and p2 is undefined. We can now handle the data that did return successfully.

If we want to return something other than undefined, we can modify the catch to return custom error text or the original response:

...
.catch(function (err) {
    return err;
});
...

//Promise.all [ 'one', 'two was rejected', 'three' ]

This simply returns the original rejection value to our promise instead of undefined.

Putting it All Together

Our example is made up, of course, as it depends on hour we are making the API request. We can do a little better, though, by moving our promise generation to a function, similar to how we’d typically handle this in real code. For a better example, I have created a function apiRequest() that will act like our http client.

We will pass a sample string for the ‘url’, and just resolve the promise with that same string. To trigger a ‘failed’ http request, I’ll add some logic to reject the promise if no url is passed to the function. Let’s try it out:

function apiRequest(url) {
    return new Promise(function (resolve, reject) {

        //our fake api simply returns the string passed as the 'url'
        if (url) {
            resolve(url);
        } else {
            //if no url is passed to the function, it will fail
            reject('apiRequest failed!');
        }
    })
    .catch(function(err){
        //return error;
        return err;
    });
}

var p1 = apiRequest('urlOne');

//this one will fail
var p2 = apiRequest();

var p3 = apiRequest('urlThree');

Promise.all([p1, p2, p3])
.then(function(res){
    console.log('Promise.all', res);
})
.catch(function(err){
    console.error('err', err);
});

//Promise.all [ 'urlOne', 'apiRequest failed!', 'urlThree' ]

The Promise.all() completes with the results or error message.

In addition to being more similar to a real world implementation, our .catch() is being added to each API request instead of just p2.

A Word of Caution

A major complaint of JS Promises is that it can be pretty simple to ‘swallow errors’, meaning that a sneaky catch() can handle an error without our code ever finding it. This could be the case here if we were expecting all promises generated with apiRequest() to fail or reject any time the actual http request fails.

As it stands now, or previous case has a catch at the end of the Promise.all which will never actually catch anything since it is being handled in a catch in the apiRequest function, so it is important to note this implementation would be for our specific use-case.

More Help

If you need more info, check out my notes on JavaScript Promises.

And if you would like an actual demo of using Promises to make API requests in an app, see how to use promises in a cross platform mobile app.