12

Update: this issue was a result of jQuery 1.7 vs 1.8. Do not ever use promises in 1.7 beacuse they aren't chainable with returning a promise inside a .then. 1.8 looks like they didn't mess it up.

http://jsfiddle.net/delvarworld/28TDM/

// make a promise
var deferred = $.Deferred();
promise = deferred.promise();

// return a promise, that after 1 second, is rejected
promise.then(function(){
    var t = $.Deferred();
    setTimeout(function() {
        console.log('rejecting...');
        t.reject();
    }, 1000);

    return t.promise();
});

// if that promise is successful, do this
promise.then(function() {
    console.log('i should never be called');
})

// if it errors, do this
promise.fail(function() {
    console.log('i should be called');
});

deferred.resolve();

Expected: 'i should be called'

Actual: 'i should never be called'

Problem: I want to chain callbacks and have any one of them be able to break the chain and trigger the fail function, and skip the other chained callbacks. I don't understand why all of the thens are triggered and the fail is not triggered.

I'm coming from NodeJS's Q library, so I tried it with .then first. However, changing it to .pipe has no effect.

Andy Ray
  • 26,451
  • 11
  • 86
  • 123
  • Returning `t` from `.then()` doesn't do anything. Rejecting `t` also doesn't affect the original deferred object in any way. Callbacks from `.then` aren't called until after the deferred object they are applied to are either resolved or rejected. Once they are resolved or rejected, they can't be rejected or resolved again. – Kevin B Aug 27 '12 at 21:44
  • To make this easier to understand, replace `.then` with `.done` since in your case they do exactly the same thing. – Kevin B Aug 27 '12 at 21:46
  • At least in Q, I believe, returning a promise inside of `then` adds it to the chain. what's the correct way to chain otherwise? – Andy Ray Aug 27 '12 at 21:47
  • Related: [Problems inherent to jQuery $.Deferred](http://stackoverflow.com/q/23744612/1048572), especially when you come with your expectations from Q. – Bergi Mar 25 '15 at 20:51

2 Answers2

12

You aren't re-defining the value of promise, try this:

http://jsfiddle.net/28TDM/1/

var deferred = $.Deferred();
promise = deferred.promise();

promise = promise.then(function(){
    var t = $.Deferred();
    setTimeout(function() {
        console.log('rejecting...');
        t.reject();
    }, 1000);

    return t.promise();
});

promise.then(function() {
    console.log('i should never be called');
})

promise.fail(function() {
    console.log('i should be called');
});

deferred.resolve();

Apparently it does work the way you thought it did, it just isn't documented https://api.jquery.com/deferred.then. Very cool. This is new functionality added in jQuery 1.8.0, more than likely they just aren't done updating the documentation.

Kevin B
  • 92,700
  • 15
  • 158
  • 170
  • i'm still having an issue with this flow but having trouble making a generalized jsfiddle for it. i'm wondering if this fiddle is just working from coincidence. – Andy Ray Aug 28 '12 at 00:05
  • @AndyRay If it helps any, this is the change that made my fiddle work: http://bugs.jquery.com/ticket/11010 – Kevin B Aug 28 '12 at 05:37
  • Also, if you use `.pipe` in place of `.then` it will also work. http://jsfiddle.net/28TDM/2/ Then, if you chain it rather than going back to the `promise` variable, you don't have to re-define `promise`. http://jsfiddle.net/28TDM/3/ – Kevin B Aug 28 '12 at 06:01
  • UPDATE: I have found the actual issue. We were using jQuery 1.7. Promises in 1.7 are broken and awful. They are fixed in 1.8 and it looks like pipe is deprecated, thank god. – Andy Ray Sep 21 '12 at 23:06
  • `i should never be called` IS called. you don't really need a demo page since you can just copy the code to the browser inspector and run it.. – vsync Apr 02 '14 at 16:34
  • @vsync that depends on what website you're on. SO here still uses jquery 1.7.1 and the answer is for jquery 1.8+ – Kevin B Apr 02 '14 at 17:23
  • @KevinB - checked on my website which has 1.11.. it first says `rejecting...` then `i should never be called` – vsync Apr 02 '14 at 18:05
  • That's not what i see here: http://jsfiddle.net/28TDM/14/ does `$.fn.jquery` give you 1.11? – Kevin B Apr 02 '14 at 18:07
  • i'm confused. your image shows exactly what it should. `rejecting...` then `i should be called` – Kevin B Apr 02 '14 at 18:14
  • 1
    haa opss sorry, my brain read it as "I should never be called". sorry again. excellent work! – vsync Apr 02 '14 at 18:15
1

IMHO, you're not chaining anything. Your 2nd .then is attached to the same promise as that the first .then is attached to.

Why?

Notice that, then will always RETURN the new promise, rather than change the promise it being attached to. It doesn't have side effect.

For example:

var promiseX = promiseA
                 .then(function() { return promiseB; })
promiseX.then(function() { return promiseC; });

promiseA won't change its value after being attached then; it'll keep as is.

promiseX will be the return value of the 1st then, that is, promiseB.

So the 2nd then is actually attached to promiseB.

And this is exactly what @Kevin B did in his answer.


Another solution is, since .then will return the new promise, you can chain the .then functions like below.

var promiseX = promiseA
                 .then(function() { return promiseB; })
                 .then(function() { return promiseC; });

This time, the 1st then is attached to promiseA, and guess to which promise is the 2nd then being attached to?

You're right. It's promiseB, not promiseA. Because the 2nd then is actually being attached to the return value of the 1st then, i.e., promiseB.

And finally the 2nd then's return value is assigned to promiseX, so promiseX equals promiseC.

Ok, get back to the OP's question. The following code is my answer.

var deferred = $.Deferred();
promise = deferred.promise(); // this is the first promise

promise.then(function(){ // callbacks for 1st promise
    var t = $.Deferred();
    setTimeout(function() {
        console.log('rejecting...');
        t.reject();
    }, 1000);
    return t.promise(); // this is the 2nd promise
    // return $.Deferred().reject(); // To reject immediately.
}).then(function() { // callbacks for 2nd promise
    console.log('i should never be called');
}, function() {
    console.log('i should be called');
})

deferred.resolve();
lzl124631x
  • 3,445
  • 2
  • 20
  • 38
  • The downvote is probably because `.then` returns a new promise. You are incorrect. – doug65536 Aug 22 '16 at 19:53
  • Of course `.then` return a new promise, I knew it. But look at the example from OP, `i should never be called` is attached to the promise corresponding to `deferred` not `t`. But the OP expected that he/she rejected the promise `t` and the 2nd `then` is attached to `t.promise` (but it's not). That's what I meant. Look at my code, the 2nd `then` is attached to `t.promise`, that's exactly what the OP expected. Isn't it? – lzl124631x Aug 23 '16 at 06:24
  • Oh. The wording of your second sentence in the answer seems to imply otherwise. The second `.then` is not attached to the same promise as the first `then`. Even if the first "then callback" didn't return a promise, the second then would still be attached to a different `Deferred` instance, because `.then` always returns a new deferred. [Example on jsfiddle](https://jsfiddle.net/pfdeewhv/). – doug65536 Aug 23 '16 at 09:30
  • Oh! I think the downvote was from someone that misunderstood that sentence the same way I did. You meant *the OP's* second then is attached to the same promise. It is easy to mistakenly interpret your statement as saying `.then` chains like a selector, where the original promise chains though. +1. – doug65536 Aug 23 '16 at 09:40
  • @doug65536. Your comment before last comment is wrong. Yes, `.then` always return a new promise, but OP didn't assign the returned promise to any variable, so the variable `promise` will still be equal to `deferred.promise()` and hence 2nd `then` is attached to `deferred.promise()`. And that's why @Kevin B, assigned the promise returned by the first `then` back to `promise`, which updates the variable `promise` to `t.promise()`. In my answer, the first `then` returns a new promise (`t.promise`), to which the 2nd `then` is attached. Thanks for your upvote ;) – lzl124631x Aug 23 '16 at 12:43
  • @doug65536 Thanks for pointing out the problem. I'm not a native English speaker so maybe my answer was misleading. I elaborated what I meant in the updated answer. – lzl124631x Aug 23 '16 at 12:55
  • I gave up on javascript questions a while ago. Too many Duning Kruger "experts" roaming around. Correct js answers get downvoted all the time. – doug65536 Dec 01 '16 at 03:21
  • The 10 upvote accepted answer is an "oh neat that does work!" with one assignment added in and complaint about documentation. Would be nice if he removed the misinformation comments on the question though. – doug65536 Dec 01 '16 at 03:32
  • Thanks. The accepted answer does work, so does mine. I also explained some details about `then` and the 2nd solution "then chain". I suspect the downvoter didn't look at my solution carefully. Thanks for listening to my complain ;) – lzl124631x Dec 01 '16 at 03:37