Promise Chaining

Let’s suppose that you have a sequence of asynchronous tasks to carry out one after another. For example, loading scripts.

The JavaScript promises to introduce a couple of receipts to perform that. This chapter is dedicated to promise chaining, which is demonstrated in the example below:

new Promise(function (resolve, reject) {
  setTimeout(() => resolve(2), 1500); // (*)
}).then(function (result) { // (**)
  console.log(result); // 2
  return result + 2;
}).then(function (result) { // (***)
  console.log(result); // 4
  return result + 2;
}).then(function (result) {
  console.log(result); // 6
  return result + 2;
});

The notion is that the result passes through the chain of .then handlers.

The flow is as follows:

  1. The first promise will resolve in a second (*)
  2. After that, the .then handler is invoked (**)
  3. The value it returns is passed to the further .then handler, and so on.

You can notice that the result is passed along the unique chain of handlers, and there is an order of the following alert calls: 2 → 4 → 8.

The whole chain works as a call to promise.then returns a promise in a way that it allows you to call the upcoming .then on it.

Whenever a handler returns a value, it is the result of the promise, and the upcoming then should be called with it.

Please remember that adding many .then to a promise does not yet mean that it’s chaining.

For instance:

let promise = new Promise(function (resolve, reject) {
  setTimeout(() => resolve(2), 1500);
});
promise.then(function (result) {
  console.log(result); // 2
  return result + 2;
});
promise.then(function (result) {
  console.log(result); // 2
  return result + 2;
});
promise.then(function (result) {
  console.log(result); // 2
  return result + 2;
});

In this example, there are just several handlers to a promise. They are not passing the result to each other yet process it separately.

Now, look at the following picture and compare it with the chaining picture that was shown above:

So, the overall .then on the same promise get the same result: that promise’s result. So the console.log in this code shows the same 2.

In practice, developers need this rarely, compared with chaining, which is a quite common practice.

Returning Promises

A handler that is applied for .then(handler) is capable of creating and returning a promise.

In such a situation, the next handlers wait until it settles, getting its result after that.

It is visualized in the example below:

new Promise(function (resolve, reject) {
  setTimeout(() => resolve(2), 1500);
}).then(function (result) {
  console.log(result); // 2
  return new Promise((resolve, reject) => { // (*)
    setTimeout(() => resolve(result + 2), 1500);
  });
}).then(function (result) { // (**)
  console.log(result); // 4
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(result + 2), 1500);
  });
}).then(function (result) {
  console.log(result); // 6
});

In this example, the initial .then shows 2, returning new Promise(…) in the line (*). After just a second, it resolves; after that, the result passes on to the handler of the second .then. That handles is placed on the line (**) and demonstrates 4, doing the same thing.

Its output will be equivalent to the previous example: 2 → 4 → 6. The difference is that now there is a second delay between the console.log calls.

Returning the promises allows building chains of asynchronous actions.

Example: loadScript

In this part, we are going to use this feature in the promisified loadScript, described in the chapter Promises, applied for loading scripts one by one, in an order:

loadScript("/promiseChaining/oneScript.js")
  .then(function (script) {
    return loadScript("/promiseChaining/twoScript.js");
  })
  .then(function (script) {
    return loadScript("/promiseChaining/threeScript.js");
  })
  .then(function (script) {
    // use the functions declared in the scripts
    // to show that they are really loaded
    oneScript();
    twoScript();
    threeScript();
  });

The usage of arrow functions can make this code shorter, like here:

loadScript("/promiseChaining/oneScript.js")
  .then(script => loadScript("/promiseChaining/twoScript.js"))
  .then(script => loadScript("/promiseChaining/threeScript.js"))
  .then(script => {
    // scripts are loaded, we can use some functions declared there
    oneScript();
    twoScript();
    threeScript();
  });

In the example above, every loadScript returns a promise, and the upcoming .then runs at the time it resolves. After that, it initiates the next script’s loading. So, we can assume that scripts run in a sequence.

It is possible to add more asynchronous action to the chain. Another fundamental thing for keeping in mind is that the code goes down, not to the right. No signs of the “pyramid of doom” exist here.

You can add .then to every loadScript, as follows;

loadScript("/promiseChaining/oneScript.js").then(script1 => {
  loadScript("/promiseChaining/twoScript.js").then(script2 => {
    loadScript("/promiseChaining/threeScript.js").then(script3 => {
      // this function has access to variables script1, script2 and script3
      oneScript();
      twoScript();
      threeScript();
    });
  });
});

The code above also loads three scripts in order. But, notice that it grows to the right. And here occurs the same problem as with callbacks.

Frequently, when developers start just using promises, not knowing about chaining yet, they write it this way.

Anyway, chaining is much more preferable.

In cases when the nested function has access to the outer scope, it is acceptable to write .then directly.

In the case above, most nested callback has access to the overall variables: script1, script2, script3. Please, take into consideration that this is an exception rather than a rule.

Thenables

A handler can return not only exactly a promise but a thenable” object, which represents an arbitrary object obtaining a method .then. It will be dealt with in the same way as a promise.

The notion is that the third-party libraries possibly can perform their own “promise-compatible” objects. They may obtain a broad set of methods, at the same time being compatible with native promises as they perform .then. For a better perception of thenable objects, look at the example below:

class Thenable {
  constructor(number) {
    this.number = number;
  }
  then(resolve, reject) {
    console.log(resolve); // function() { native code }
    // resolve with this.number+2 after the 2 second
    setTimeout(() => resolve(this.number + 2), 2000); 
  }
}

new Promise(resolve => resolve(2))
  .then(result => {
    return new Thenable(result); 
  })
  .then(console.log); // shows 4 after 2000ms

Bigger Example: fetch

Frontend programmers use promises for network requests. In this part, we will consider an example of that.

Let’s use the fetch method for loading the information about the user from the remote server. Its basic syntax looks like this:

let promise = fetch(url);

The syntax above makes a network request to the url, returning a promise. The promise resolves with a response object in case the remote server responds with headers before the entire response is downloaded.

For reading the full response, it is necessary to call the method response.text(). Accordingly, this method returns a promise, which resolves when the entire text is downloaded from the remote server with that text as a consequence.

In the example below is demonstrated a request to the site.json and loading of the text from the server:

fetch('/promiseChaining/site.json')
  // .then below works when the remote server responds
  .then(function (response) {
    // response.text() returned a new promise
    //that resolved with the full response text
    // when it loaded
    return response.text();
  })
  .then(function (text) {
    // and here's the content of the remote file
    console.log(text); // {"name": "W3Docs", isBook: true}
  });

The response object returned from the fetch involves the method response.json(), which can read the remote data parsing it as JSON.

Now, let’s try switching to it using arrow functions:

// same as above, but response.json() parses remote content as JSON
fetch('/promiseChaining/site.json')
  .then(response => response.json())
  .then(site => console.log(site.name)); // W3Docs, got site name

Now, let’s see how the loaded user case works.

For example, you can make another request to GitHub, load the user profile, as well as show the avatar, as follows:

// Make a request for user.json
fetch('/promiseChaining/user.json')
  // Load it as json
  .then(response => response.json())
  // Make a request to GitHub
  .then(user => fetch(`https://api.github.com/users/${user.name}`))
  // Load the response as json
  .then(response => response.json())
  // Show the avatar image for 2 seconds
  .then(githubUser => {
    let img = document.createElement('img');
    img.src = githubUser.avatarUrl;
    img.className = "promiseAvatarExample";
    document.body.append(img);
    setTimeout(() => img.remove(), 2000); // (*)
  });

The code above works, but an error exists in it.

As a rule, an asynchronous action always returns a promise. It allows planning actions after it. Even if you are not going to extend the chain currently, you might need it later.

Finally, here is the option of splitting the code into reusable functions:

function loadJson(url) {
  return fetch(url)
    .then(response => response.json());
}

function loadGithubUser(name) {
  return fetch(`https://api.github.com/users/${name}`)
    .then(response => response.json());
}

function showAvatar(user) {
  return new Promise(function (resolve, reject) {
    let img = document.createElement('img');
    img.src = user.avatarUrl;
    img.className = "promiseAvatarExample";
    document.body.append(img);

    setTimeout(() => {
      img.remove();
      resolve(user);
    }, 2000);
  });
}
// Use them:
loadJson('/promiseChaining/user.json')
  .then(user => loadGithubUser(user.name))
  .then(showAvatar)
  .then(user => console.log(`Finished showing ${user.name}`));  // ...

Summary

JavaScript promises can be chained together for using the settled promises (resolved or rejected), as well as the promises’ values to control the flow of execution. It is useful when there is a sequence of asynchronous operations to perform that each of them depends on the results of the previous operation. The next step of the chain will not be performed until the preceding one is resolved. The chains of promises also involve error propagation. The chain of promises may be handled once.

As a rule, chaining promises is performed with the following methods of the promise object prototype: .then and .catch.

The .then method determines the action to run when a promise is fulfilled. The .catch method determines what to do in case the promise is rejected.




Do you find this helpful?

Related articles