Julien Zakaib
Julien Zakaib

Reputation: 194

jQuery Deferred continue to resolve after fail handler

I have a promise that can resolve or reject. I want to do something specific in those cases, and then continue resolving the promise chain (essentially I want to "catch" the rejected promise, do something, then continue resolving).

Here's a functional snippet that shows the issue I'm running into :

var def = $.Deferred();
def.then(
  function() {
    console.log('first success handler');
  },
  function() {
    console.log('first fail handler');
    return $.Deferred().resolve();
  }
);
def.then(
  function() {
    console.log('second success handler');
  },
  function() {
    console.log('second fail handler');
  }
);
def.done(function() {
  console.log('done handler');
});

def.reject();
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

Expected result :

first fail handler
second success handler
done handler

Current result :

first fail handler
second fail handler

According to jQuery docs :

As of jQuery 1.8, the deferred.then() method returns a new promise that can filter the status and values of a deferred through a function[...]. These filter functions can return a new value to be passed along to the promise's .done() or .fail() callbacks, or they can return another observable object (Deferred, Promise, etc) which will pass its resolved / rejected status and values to the promise's callbacks.

So I have no idea why this is not working. I expected the returned resolved promise in the first fail handler to allow the rest of the promise chain to continue resolving.

Upvotes: 0

Views: 2407

Answers (3)

Bergi
Bergi

Reputation: 664185

The important part of the docs is

the deferred.then() method returns a new promise

You were however throwing away that return value, and did invoke the next .then(…) on the original def.

You will want to use

var p = $.Deferred().reject().promise();

p.then(function() {
    console.log('first success handler');
}, function() {
    console.log('first fail handler');
    return $.Deferred().resolve();
}).then(function() {
//^^^^^ chain directly on the result
    console.log('second success handler');
}, function() {
    console.log('second fail handler');
}).then(function() {
//^^^^^
  console.log('done handler');
});

Upvotes: 1

Roamer-1888
Roamer-1888

Reputation: 19298

The pattern ...

var def = $.Deferred();
def.then(successHandler, errorHandler);
def.then(successHandler, errorHandler);
// etc.

... forms two (or more) branches, each then() being dependent only on def. Each branch possesses independent filtering power, but it is not exploited.

That is very different from ...

var def = $.Deferred();
def.then(successHandler, errorHandler).then(successHandler, errorHandler);  // etc.

... which forms a single chain (without branching). The first then() is dependent on def, and the second then() dependent on the first then(). Here, the filtering power of the first then() is exploited by chaining another then() (and so on).

Therefore, you will get the expected output by converting the code in the question to the second pattern :

var def = $.Deferred();

def.then(function() {
    console.log('first success handler');
}, function() {
    console.log('first fail handler'); // (1)
    return $.Deferred().resolve();
}).then(function() {
    console.log('second success handler'); // (2)
}, function() {
    console.log('second fail handler');
}).done(function() {
    console.log('done handler'); // (3)
});

def.reject();   

In a nutshell, that's what promise chaining is all about.

But don't forget about branching completely. In certain circumstances, it is essential. In this answer for example, batchRequests() returns _p, which can be further chained by the caller (that's one branch), but also forms its own private branch with _p.then(..., ...). Don't worry if you can't follow it completely - it's fairly complicated - for now trust me that branching is an essential part of the solution.

Upvotes: 4

rasmeister
rasmeister

Reputation: 1996

What your code is effectively doing is queuing two handlers (through the then functions). When the deferred is rejected it can no longer be altered.

When the Deferred is rejected, the first then handler fires writing your first console message.

The line return $.Deferred().resolve(); will create a new resolved Deferred but it is not "return"ed to your second then since that handler is already tied to the first Deferred instance.

Consequently, your second then handler will now fire also hitting the failFilter (the handler for when the Deferred is rejected).

One approach you could take is to pass a value when you reject (or resolve) your Deferred. You can then receive that response and take remedial action as shown in this (somewhat contrived) sample:

  <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
</head>
<body>

<div id="test"></div>

<script>
    var def = $.Deferred();
    def.then(
            function () {
                console.log('first success handler');
            },
            function (response) {
                console.log('first fail handler');
                console.log(response);
            }
    );
    def.then(
            function () {
                console.log('second success handler');
            },
            function (response) {
                if (response === 999) {
                    // do something to recover from earlier reject
                    console.log('recovery action initiated');
                } else {
                    console.log('second fail handler');
                }
            }
    );
    def.done(function () {
        console.log('done handler');
    });

    def.reject(999);
</script>

</body>
</html>

I know of no way to interject between the two then handlers, they are simply two queued handlers to the same Deferred.

Upvotes: 0

Related Questions