6

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?

ekkis
  • 9,804
  • 13
  • 55
  • 105
  • `getData1` should return `$.ajax` promise. – hsz Jun 08 '15 at 09:00
  • First thought: you can keep passing the promise around (as a second param), on success you complete the promise, on failure you run the callback. – Ja͢ck Jun 08 '15 at 09:02
  • I think you can't do that on asyncrhronous function. Why not use recursive callback and simple if-else? – dann Jun 08 '15 at 09:17
  • What is `$.with()`? Did you mean `$.when()`? – jfriend00 Jun 08 '15 at 10:44
  • @jfriend00, yes I did. I've fixed it, thanks. – ekkis Jun 08 '15 at 16:57
  • 1
    `$.when()` is not needed when you only have one promise. If you fix `getData1()` to return a promise, then you can just do `getData1()`.then(...)`. – jfriend00 Jun 08 '15 at 17:07
  • Have a look at the [differences between `deferred.then(null,func)` and `deferred.fail(func)` in jQuery?](http://stackoverflow.com/q/30719183/1048572) – Bergi Jun 14 '15 at 22:09

2 Answers2

11

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');
});
Roamer-1888
  • 19,138
  • 5
  • 33
  • 44
  • that's awesome @Roamer-1888. it's fascinating I haven't seen any examples of this kind of solution out there, but this is very clean. I will accept it as a solution when I've verified it works. +1 – ekkis Jun 08 '15 at 17:22
  • I have answered similar several times recently but the questions have all been very dissimilarly phrased, which is probably why they are difficult to find. You almost need to know the answer in order to know what to search for, which is why I keep providing new answers rather than links to previous. – Roamer-1888 Jun 08 '15 at 18:16
  • 1
    BTW, credit goes to @Bergi for putting me right on this approach in the first place. Sorry, no time right now to dig for the link. – Roamer-1888 Jun 08 '15 at 18:26
  • @Roamer-1888: Heh, good to hear :-) However I've used the `reduce` approach in a thousand places now, there's not yet a canonical we could link. – Bergi Jun 14 '15 at 22:02
  • @Bergi, totally agree - we need a canonical, and it needs to cover at least the two variants - break-on-first-success and break-on-first-failure. I guess other variants should be covered, break-on-first-settled and continue-regardless. And others? – Roamer-1888 Jun 14 '15 at 22:16
  • Im sorry to necro this, but shouldn't it be "return $.Deferred().reject()" instead of "$.Deferred.reject()"? And may you add an example of getDatax() ? – Aiyion.Prime Jul 10 '15 at 17:45
  • Im sorry, i didn't mean to forget the return. I meant your first code block and wondered about the parentheses. – Aiyion.Prime Jul 10 '15 at 18:58
  • @Aiyion.Prime, you are right. I forgot the parentheses, thanks. – Roamer-1888 Jul 10 '15 at 19:01
1

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.

  • Keep in mind that async/await is not supported by all browsers at the moment. Here is an useful table for checking this and other functionalities aswell: https://caniuse.com/#feat=async-functions – CarlosCarucce Jun 15 '20 at 11:46