Implementing a fallback using promises

2.8k views Asked by At

it is a common pattern that we cascade across a list of sources of data with the first success breaking the chain like this:

var data = getData1();
if (!data) data = getData2();
if (!data) data = getData3();

et cetera. if the getDataN() functions are asynchronous, however, it leads us to 'callback hell':

var data;
getData1(function() {
    getData2(function () {
        getData3(function () { alert('not found'); })
    })
});

where the implementations may look something like:

function getData1(callback) {
    $.ajax({
        url: '/my/url/1/',
        success: function(ret) { data = ret },
        error: callback
    });
 }

...with promises I would expect to write something like this:

$.when(getData1())
   .then(function (x) { data = x; })
   .fail(function () { return getData2(); })
   .then(function (x) { data = x; }) 
   .fail(function () { return getData3(); })
   .then(function (x) { data = x; });

where the second .then actually refers to the return value of the first .fail, which is itself a promise, and which I understood was chained in as the input to the succeeding chain step.

clearly I'm wrong but what is the correct way to write this?

2

There are 2 answers

8
Roamer-1888 On BEST ANSWER

In most promise libs, you could chain .fail() or .catch() as in @mido22's answer, but jQuery's .fail() doesn't "handle" an error as such. It is guaranteed always to pass on the input promise (with unaltered state), which would not allow the required "break" of the cascade if/when success happens.

The only jQuery Promise method that can return a promise with a different state (or different value/reason) is .then().

Therefore you could write a chain which continues on error by specifying the next step as a then's error handler at each stage.

function getDataUntilAsyncSuccess() {
    return $.Deferred().reject()
        .then(null, getData1)
        .then(null, getData2)
        .then(null, getData3);
}
//The nulls ensure that success at any stage will pass straight through to the first non-null success handler.

getDataUntilAsyncSuccess().then(function (x) {
    //"success" data is available here as `x`
}, function (err) {
    console.log('not found');
});

But in practice, you might more typically create an array of functions or data objects which are invoked in turn with the help of Array method .reduce().

For example :

var fns = [
    getData1,
    getData2,
    getData3,
    getData4,
    getData5
];      

function getDataUntilAsyncSuccess(data) {
    return data.reduce(function(promise, fn) {
        return promise.then(null, fn);
    }, $.Deferred().reject());// a rejected promise to get the chain started
}

getDataUntilAsyncSuccess(fns).then(function (x) {
    //"success" data is available here as `x`
}, function (err) {
    console.log('not found');
});

Or, as is probably a better solution here :

var urls = [
    '/path/1/',
    '/path/2/',
    '/path/3/',
    '/path/4/',
    '/path/5/'
];      

function getDataUntilAsyncSuccess(data) {
    return data.reduce(function(promise, url) {
        return promise.then(null, function() {
            return getData(url);// call a generalised `getData()` function that accepts a URL.
        });
    }, $.Deferred().reject());// a rejected promise to get the chain started
}

getDataUntilAsyncSuccess(urls).then(function (x) {
    //"success" data is available here as `x`
}, function (err) {
    console.log('not found');
});
1
Florian Mayer On

As a beginner, stumbling across the same problem, I just realized how much simpler this has become with async and await:

The synchronous pattern

var data = getData1();
if (!data) data = getData2();
if (!data) data = getData3();

can now easily be applied to asynchronous code:

let data = await getData1();
if (!data) data = await getData2();
if (!data) data = await getData3();

Just remember to add an async to the function that this code is used in.