Sam
Sam

Reputation: 4339

Stopping JQuery promise with failure from propagating into wrapper

My application has a bunch of $.ajax calls that mostly have .done and .fail functions.

I want to write a wrapper around the Ajax call, so that if a function does not have a .fail function, or if the .fail function does not handle a given error (for example it only handles 404, but I get back a 403), then I want the wrapper to handle it as a fallback.

Here's what I've written:

function DoAjax(fn, errorText) {
    var deferred = $.Deferred();
    var result = fn();
    if (result === undefined)
        throw 'Function passed into DoAjax has not returned a promise.';
    result.always(function () {
        deferred.resolve(result);
    });
    result.fail(function () {
        alert('fail');
        deferred.fail();
    });
    return deferred.promise();
}

It works mostly as I intend, except that the fail will execute twice - once in the method DoAjax, the other at the site of the Ajax call.

I execute DoAjax like this:

DoAjax(function () {
        var ajaxCall = $.ajax({
            method: "GET",
            url: baseUrl + "api/test"
        });

        ajaxCall.fail(function() { alert('failed the call'); });

        return ajaxCall;
});

I believe I can probably accomplish this by manually creating a $.deferred and then doing the $.ajax call around that, but would rather not have another wrapper around it.

I'm using jQuery 2.2.

Upvotes: 1

Views: 130

Answers (1)

Roamer-1888
Roamer-1888

Reputation: 19288

It's time to do some hard-core learning. Several things need to be taken on board :

  1. .done() and .fail() are just jQuery.ajax() options without the socalled "filtering power" of .then(successCallback, errorCallback) (or .then()/.catch() in jQuery 3+). In other words, whatever is returned from .done() and .fail() callbacks has no downstream effect on a jqxhr promise chain.
  2. At jQuery <3, an errorCallback doesn't automatically catch an error. The default behaviour is to remain on the promise chain's error path. In order to give an errorCallback 'catch' behaviour, then you must return a resolved promise from it. At jQuery 3+, an errorCallback catches by default but you can either return a rejected promise or throw to propagate the error condision.
  3. Your fn(), in this case, returns jqxhr, not a result. Therefore, as written, the if(result === undefined) test will always return false. The result that the jqxhr eventually delivers can't be tested in this way.
  4. If you are uncertain whether a function returns a value or a Promise, then wrap the returned value/object in jQuery.when(...), eg $.when(fn()).
  5. With (3) taken care of, you can chain directly from the promise returned by jQuery.when(...). There's no need for an explicit $.Deferred().

Try this :

var baseUrl = '/';
function DoAjax(fn) {
    return $.when(fn()) // no need to test for fn() not returning a promise. `$.when(fn())` forces any non-Promise to Promise.
    .then(function(response) {
        console.log('success!', response);
    }).then(null, function(err) {
        console.error('B', err); // (LOG_2)
    });
}

DoAjax(function () {
    return $.ajax({
        method: "GET",
        url: baseUrl + "api/test"
    }).then(null, function(jqxhr, textStatus) {
        var err = new Error('Whoops! ' + jqxhr.status + ': ' + textStatus);
        console.error('A', err);

        // "throw behaviour"
        return err; // propagate the error (jQuery <3) or `throw err;` (jQuery 3+)

        // "catch behaviour"
        // return $.when('default response'); // "2" mutate failure into success (jQuery <3) or `return 'default response';` (jQuery 3+)
    });
});

Fiddle

Now try swapping out the lines labelled "propagate behaviour" and "catch behaviour" and observe the difference in the log.

Don't worry if you don't get it immediately. Learning this stuff takes time.

If you have a choice, then work jQuery 3+, in which jqxhr and other Promises are far closer to javascript's native Promise than in jQuery <3.

Upvotes: 2

Related Questions