Promises Chaining

Chapter 23 17 mins

Learning outcomes:

  1. What is chaining
  2. How chaining works internally
  3. A practical example

What is chaining?

Yes, JavaScript promises are superbly amazing, but this isn't true until and unless we discover the concept of chaining. But what exactly is chaining?

Chaining refers to the notion of subsequently calling then() on the promise returned by then().

Sounds crazy? Let's discuss it!

Recall from the previous chapter that the method then() puts up a callback, internally within a queue in its promise, to be executed the moment it settles (resolves or fails).

The whole idea sounds quite sensible: 'doSomeAsyncTask() and then once it completes, do this with the result'. In very simple words, then() puts up a callback to fire only once its owner promise resolves.

Talking about the return value, appreciate the fact that then() returns a new promise whose settlement depends on the invocation and, consequently, completion of the provided callback argument.

When we invoke then() on a promise p, with a given argument callback, a new promise is created and returned that only settles when the callback is fired (and this happens only once the main promise p settles).

It's like we now say: 'doSomeAsyncTask(), then do this with the result; and once this is done; then further do this'.

See how we've used the word 'then' twice here.

This return of a promise by then() is what enables subsequent then() calls to be made on its invocation expression; and thus the whole idea of chaining.

How chaining works?

Promise chaining is not that straightforward as much as it looks. Multiple checks are made internally within the then() method after which it's decided how the returned promise will settle and with what value, and that how will even subsequent promises settle down.

In this section we shall understand all the technical details to chaining promises using the then() method, before moving on to consider some real examples on the topic.

As we know from the previous Promise Basics chapter, if then() is called while its promise is still unsettled, the callbacks are queued internally - to be executed when the promise settles in the future.

Now what happens after this is that:

A new promise is created; its reference saved internally in another queue; and finally returned by the method.

But why do we need to return another promise?

Well, think logically - if an async task wrapped up in a promise object completes and likewise its then() callback fires, it could be the case that we need to perform a second async operation at this point.

It's not surprising to say that: 'doFirstAsyncTask() and then once it completes doSecondAsyncTask()'.

Thereby it sounds sensible to return a new promise by calling then(), to placehold the eventual completion of the respective callback's actions.

According to the spec, this returned promise is given the name derived promise. In the discussion that follows, we'll use this name to refer to any promise returned by then().

A reference to this derived promise is saved internally inside the main promise, just like the onFulfilled (or onRejected) callback is saved, given that the main promise is still unsettled.

Each invocation of then() creates a separate promise that depends on the callbacks provided to that specific then() method.

What this means is that multiple then() calls on a promise p will each return a completely new promise, which will settle when the corresponding callback's actions are complete.

For example calling p.then(f1, r1) will return a promise which will depend on the completion of the action f1 (or r1) for its resolution (or rejection). Similarly calling p.then(f2, r2) will return another promise, but this time dependent upon the callback f2 (or r2).

This signifies the fact that each call to then() with given onFulfilled and onRejected arguments, queues up the callbacks and along with them the reference to the derived promise, internally within the main promise.

Let's also talk about the case when then() is called after a promise's settlement.

First the respective callback is executed; then its returned or thrown value is made the value of the derived promise and used to, therefore, settle it; before finally returning this derived promise.

If the callback returns another promise object, a slightly different mechanism is employed, as we shall see below.

Returned promise's settlement

Essentially, when a callback passed to then() completely executes, its corresponding derived promise is settled down with its return (or thrown) value.

The only case where things are a bit different is when the callback returns a promise itself - in that case the returned promise governs the derived promise.

There are basically three possible outcomes of firing the callback passed to then():

  1. Returning a non-promise value
  2. Throwing an exception
  3. Returning a promise

Let's discuss all these three possible outcomes in detail.

A non-promise value

If the callback returns a non-promise value - for example a number, a string, an array etc. - the returned promise is fulfilled with that value.

Following is an illustration:

var p = new Promise(function(resolve, reject) {
    resolve("Data1");
});

var p2 = p.then(function(data) {
    // callback returns a non-promise value
    // in this case a string
    return "Data2";
});

console.log(p2);
Promise {<resolved>: "Data2"}

A promise p is created and immediately fulfilled in line 2, after which the then() method is called on it. Since p is settled, the passed-in callback is executed right away, returning the value "Data2". This results in the derived promise, saved in p2, to be fulfilled with the value "Data2".

Consider another example below:

var p = new Promise(function(resolve, reject) {
    reject("Sorry");
});

var p2 = p.then(null, function(data) {
    // callback returns a non-promise value
    // in this case a string
    return "OK";
});

console.log(p2);
Promise {<resolved>: "OK"}

As before, a promise p is created but this time immediately rejected in line 2, after which the then() method is called on it. Since p is settled, the passed-in callback is executed right away, returning the value "OK". This results in the derived promise to be fulfilled, once again, with the value "OK".

It's simple:

Regardless of whenever the callback fires, if it returns a non-promise value, it will fulfill the corresponding derived promise with that value.

An exception thrown

If the callback throws an exception - for example throw new Error("Sorry") - the returned promise is rejected with that value (or better to say, with that reason).

Following is an illustration:

var p = new Promise(function(resolve, reject) {
    resolve("OK");
});

var p2 = p.then(function(data) {
    // callback throws an error
    throw "Sorry";
});

console.log(p2);
Promise {<rejected>: "Sorry"}

A promise p is created and immediately fulfilled in line 2, after which the then() method is called on it. Since p is settled, the passed-in callback is executed right away, throwing the value "Sorry". This results in the derived promise, saved in p2, to be consequently rejected with the value "Sorry".

Similar to this, consider the example below:

var p = new Promise(function(resolve, reject) {
    reject("Sorry");
});

var p2 = p.then(null, function(data) {
    // callback throws an error
    throw "Sorry again";
});

console.log(p2);
Promise {<rejected>: "Sorry"}

Everything here is the same as before, except for that the first promise p is now instead rejected with the value "Sorry".

This results in executing the second argument to then() which throws an error saying "Sorry again". Consequently the derived promise is rejected with value "Sorry again".

A promise value

If the callback returns a promise itself, we have a special case - the promise returned by then() abides by the result of this promise.

In other words - if the returned promise fulfills, the derived promise fulfills too; if it's rejected, the derived promise is rejected too; and if it's pending, the derived promise is also put in the pending state.

Consider the following code:

var p = new Promise(function(resolve, reject) {
    resolve("OK");
});

var p2 = p.then(function(data) {
    // callback returns a promise
    return new Promise(function(resolve, reject) {
        resolve(data + " Bye");
    });
});

console.log(p2);

A practical example

Yes, chaining is definitely a great way to carry out successive asynchronous procedures, one after another, in an extremely neat syntax.

We saw a couple of examples above, that illustrated the idea of chaining from all perspectives, but still left a question answered: is there a real use of chaining promises together?

Let's fulfill this question!

Can you recall the 'nested AJAX requests' task we performed in the Promises Introduction chapter? That's one application - request for a file; then once it's received, parse it; and ultimately make another request after it.

Previously we solved this task using callbacks which imposed syntactic clutter in the code, but now we'll solve it using the concept of chaining promises. Consider the code below:

Make it your habit to be able to decode long lines of code - it'll really help in this course.
To see the content of names.txt please refer to Promises Introduction.
var request1 = new Promise(function(resolve, reject) {
    var xhr = new XMLHttpRequest();
    xhr.open("GET", "names.txt", true);
    xhr.onload = function(e) {
        if (this.status === 200) { resolve(this); }
    }
    xhr.send();
});

var request2 = request1.then(function(data) {
    // extract the filename from names.txt
    var filename = data.responseText.split("\n")[1];

    // second async operation
    return new Promise(function(resolve, reject) {
        var xhr = new XMLHttpRequest();
        xhr.open("GET", filename, true);
        xhr.onload = function(e) {
            if (this.status === 200) { resolve(this); }
        }
        xhr.send();
    });
});

request2.then(function(data) {
    alert(data.responseText);
});

Live Example

In the promise request1 we dispatch out and handle our first AJAX request, to names.txt. When this request completes we call resolve() with the xhr object as an argument (in line 5).

This leads to calling the then() callback given in line 10, which processes xhr's response and, in turn, puts up a second request using it.

The argument data in line 10 is the xhr object in line 2.

The callback returns another promise, saved in request2. When this second request also completes request2 fulfills, ultimately executing the callback in line 25 which alerts the response of this second request.

To boil it down - we chained two promises request1 and request2 together with the second one dependent on the settlement of the first one. As you might appreciate now, chaining is indeed an extremely powerful and neat way to accomplish successive asynchronous operations.

Rewrite the code above, using a function makeRequest() that takes in a filepath and returns a promise wrapping up an AJAX request to it.

Following is the boilerplate set up for you:

function makeRequest(filepath) {
    // write your code here
}

The filepath argument here is a string that holds the path of the file to request for (which you can directly pass to the open() method).

function makeRequest(filepath) {
    return new Promise(function(resolve, reject) {
        var xhr = new XMLHttpRequest();
        xhr.open("GET", filepath, true);
        xhr.onload = function() {
            if (this.status === 200) { resolve(this); }
        }
        xhr.send();
    });
}

var request1 = makeRequest("names.txt");

var request2 = request1.then(function(data) {
    // extract the filename from names.txt
    var filename = data.responseText.split("\n")[1];
    return makeRequest(filename);
});

request2.then(function(data) {
    alert(data.responseText);
});

Moving on..

To summarise it all, chaining is crucial to understanding the whole purpose of promises to the core. If you can get the hang of chaining, then you can get the hand of literally anything in promises. Belive it!

In particular, you shall know that then() returns a new promise object which makes the entire idea of chaining possible; and the mechanism that goes on internally in the method whereby references to the derived promises are stored.

So to end it up, make sure you understand everything in this chapter and are hence able to clear the next quiz with a 100% score. Keep learning and keep making yourself better and better!