Search code examples
javascriptpromisechaining

Chaining Javascript promises


I'm trying to understand Promises from the MDN documentation. The first example demonstrates the then and catch methods:

// We define what to do when the promise is resolved/fulfilled with the then() call,
// and the catch() method defines what to do if the promise is rejected.
p1.then(
    // Log the fulfillment value
    function(val) {
        log.insertAdjacentHTML('beforeend', val +
            ') Promise fulfilled (<small>Async code terminated</small>)<br/>');
    })
.catch(
    // Log the rejection reason
    function(reason) {
        console.log('Handle rejected promise ('+reason+') here.');
    });

The documentation states that the then method returns a new promise, so shouln't the above code be equivalent to

var p2 = p1.then(
    // Log the fulfillment value
    function(val) {
        log.insertAdjacentHTML('beforeend', val +
            ') Promise fulfilled (<small>Async code terminated</small>)<br/>');
    });
p2.catch(
    // Log the rejection reason
    function(reason) {
        console.log('Handle rejected promise ('+reason+') here.');
    });

?

If so, then wouldn't that mean that the catch callback would be called only if the promise returned from p1.then, not the promise p1, resolved to rejected? And wouldn't I have to do this:

p1.then( /* etc. */ );
// and for rejected resolutions
p1.catch( /* etc. */ );

to catch the rejection of the promise p1 instead of chaining the catch to the then?

At first, I thought the promise returned from p1.then was the same as p1, like how jQuery does with much of its APIs. But the following clearly shows that the two promises are different.

var p1 = new Promise(function(resolve, reject) { 
  resolve("Success!");
});

console.log(p1);
// Promise { <state>: "fulfilled", <value>: "Success!" }

var p2 = p1.then(function(value) {
  console.log(value);
});
// Success!

console.log(p2); 
// Promise { <state>: "fulfilled", <value>: undefined }

Also, I played around in JSFiddle with the three approaches:

  1. p1.then(onFulfilled).catch(onRejected);
  2. p1.then(onFulfilled); p1.catch(onRejected);
  3. p1.then(onFulfilled, onRejected);

All three work. I can understand the latter two. The gist of my question is, why does the first approach also work?


Solution

  • First, a little background on how the relevant parts of promises work:

    p1.then(...) does return a new promise that is chained to the previous one. So, p1.then(...).then(...) will execute the second .then() handler only after the first one has finished. And, if the first .then() handler returns an unfulfilled promise, then it will wait for that returned promise to resolve before resolving this second promise and calling that second .then() handler.

    Secondly, when a promise chain rejects anywhere in the chain, it immediately skips down the chain (skipping any fulfilled handlers) until it gets to the first reject handler (whether that comes from a .catch() of from the second argument to .then()). This is a very important part of promise rejection because it means that you do not have to catch rejections at every level of the promise chain. You can put one .catch() at the end of the chain and any rejection that happens anywhere in the chain will go directly to that .catch().

    Also, it's worth understanding that .catch(fn) is just a shortcut for .then(null, fn). It works no differently.

    Also, keep in mind that (just like .then()) a .catch() will also return a new promise. Any if your .catch() handler does not itself throw or return a rejected promise, then the rejection will be considered "handled" and the returned promise will resolve, allowing the chain to continue on from there. This allows you to handle an error and then consciously decide if you want the chain to continue with normal fulfill logic or stay rejected.

    Now, for your specific questions...

    If so, then wouldn't that mean that the catch callback would be called only if the promise returned from p1.then, not the promise p1, resolved to rejected? And wouldn't I have to do this:

    No. Rejections propagate immediately down the chain to the next reject handler, skipping all resolve handlers. So, it will skip down the chain to the next .catch() in your example.

    This is one of the things that makes error handling so much simpler with promises. You can put .catch() at the end of the chain and it will catch errors from anywhere in the chain.

    There are sometimes reasons to intercept errors in the middle of the chain (if you want to branch and change logic on an error and then keep going with some other code) or if you want to "handle" the error and keep going. But, if your chain is all or nothing, then you can just put one .catch() at the end of the chain to catch all errors.

    It is meant to be analogous to try/catch blocks in synchronous code. Putting a .catch() at the end of the chain is like putting one try/catch block at the highest level around a bunch of synchronous code. It will catch exceptions anywhere in the code.

    All three work. I can understand the latter two. The gist of my question is, why does the first approach also work?

    All three are pretty much the same. 2 and 3 are identical. In fact, .catch(fn) is nothing more than a shortcut for .then(null, fn).

    Option 1 is slightly different because if the onFulfilled handler throws or returns a rejected promise, then the .catch() handler will get called. In the other two options, that will not be the case. Other than that one difference, it will work the same (as you observed).

    Option 1 works because rejections propagate down the chain. So, if either p1 rejects or if the onFulfilled handler returns a rejected promise or throws, then the .catch() handler will be called.