Jul
15

Mapping errors in jQuery promises / deferred

posted on 15 July 2014 in programming

Warning: Please consider that this post is over 9 years old and the content may no longer be relevant.

For those familiar with JavaScript Promises, jQuery’s Deferred.fail() handling can act unexpectedly. Standard Promises implementations allow the first catch handler to deal with the error and then return to normal execution flow (see JavaScript Promises: There and back again). jQuery on the other hand appears to execute all fail handlers, in the order they were defined with no chance to recover normal flow.

For an example, consider this code using Chrome’s built in Promises support:

function asyncSucceed() {
    return $.Deferred(function(deferred) {
        setTimeout(function() {
            console.log('async1');
            deferred.resolve();
        }, 300);
    }).promise();
};

function asyncFail() {
    return $.Deferred(function(deferred) {
        setTimeout(function() {
            console.log('async2');
            deferred.reject('some error');
        }, 100);
    }).promise();
}

asyncSucceed()
    .then(function() {
        return asyncFail();
    })
    .fail(function(err) {
        console.warn('fail1', err);
    })
    .then(function(data) {
        console.log('recovered');
    })
    .fail(function(err) {
        console.warn('fail2', err);
    });

The output of which would be:

Example output of Promises error handling

And a similar jQuery implementation:

function asyncSucceed() {
    return $.Deferred(function(deferred) {
        setTimeout(function() {
            console.log('async1');
            deferred.resolve();
        }, 300);
    }).promise();
};

function asyncFail() {
    return $.Deferred(function(deferred) {
        setTimeout(function() {
            console.log('async2');
            deferred.reject('some error');
        }, 100);
    }).promise();
}

asyncSucceed()
    .then(function() {
        return asyncFail();
    })
    .fail(function(err) {
        console.warn('fail1', err);
    })
    .then(function(data) {
        console.log('recovered');
    })
    .fail(function(err) {
        console.warn('fail2', err);
    });

The output of which would be:

Example output of jQuery.Deferred error handling

Note how Chrome’s Promises recovered after the first catch handler was executed and called the ‘recovered’ function. jQuery does not. Another small difference is that Promise.reject() and Promise.resolve() may only return a single object, whereas jQuery passes all parameters to it’s reject or resolve functions onto the then or fail handlers.

This becomes an issue in jQuery if you are trying to map different errors into a single error handler. For instance, if you’re chaining a jqXHR object from an $.ajax() call, you may want want to map jQuery’s error parameters (jqXHR, textStatus, errorThrown) to match a single error handler that just accepts an error message. Using Standard Promises this could be achieved by chaining a catch handler after the ajax request that returns a new rejected promise with the correct parameter. E.g.

function ajaxRequest() {
    return new Promise(function(resolve, reject) {
        var jqXHR = $.ajax({
            type: 'GET',
            url: 'http://example.com/some-resource',
            dataType: 'json',
            success: resolve,
            error: function(jqXHR, textStatus, errorThrown) {
                reject({
                    jqXHR: jqXHR,
                    textStatus: textStatus,
                    errorThrown: errorThrown
                });
            }
        });
    });
}

ajaxRequest()
    .catch(function(jqErr) {
        console.warn('fail1', jqErr);
        return Promise.reject(jqErr.errorThrown);
    })
    .then(function(data) {
        console.log('then1');
        if (!data)
            return Promise.reject('No data received');
    })
    .catch(function(err) {
        console.warn('fail2', err);
    });

But to do the same thing in jQuery, you need to wrap the ajax call and fail handler (mapper) in a new Deferred object, e.g.

function ajaxRequest() {
    var jqXHR = $.ajax({
        type: 'GET',
        url: 'http://example.com/some-resource',
        dataType: 'json'
    });
    return jqXHR.promise();
}

$.Deferred(function(dfd) {
    ajaxRequest()
        .done(dfd.resolve)
        .fail(function(jqXHR, textStatus, errorThrown) {
            console.warn('fail1', jqXHR, textStatus, errorThrown);
            dfd.reject(errorThrown);
        })
}).promise()
    .then(function(data) {
        console.log('then1');
        if (!data)
            return $.Deferred().reject('No data received').promise();
    })
    .fail(function(err) {
        console.warn('fail2', err);
    });