A Deeper Dive Into JavaScript Promises

Sandeep Panda

My previous article on the new JavaScript Promise API discussed the basics of Promises, error handling, and chaining concepts. It is often necessary to chain Promises together to queue asynchronous operations. But, many times we need to track the order of completion of each task to perform next operations accordingly. As asynchronous tasks can complete in any order, maintaining a sequence while performing asynchronous operations can be challenging. This article attempts to breakdown these concepts in detail.

A Closer Look at Promise Chaining

We have already seen how to chain Promises using then(). Now, let’s understand what really happens when we call then(). Consider the following code:

var newPromise = getPromise(someData).then(function(data) {  // Line 1
  return getPromise(data);  //Line 2
}).then(function(data){  //Line 3
  //use this data
});

Assume that the getPromise() function constructs a new Promise and returns it. You should note that the return type of then() is a new Promise object. In the previous example, Line 1 returns a new Promise. We have also passed a callback to then(). The value returned by the callback is used to fulfill or reject the promise. But, if the callback returns another Promise, then the new Promise (the one returned by then()) will be fulfilled only when this Promise fulfills.

We have also chained another then() on Line 3 which waits on the Promise returned on Line 2. The callback passed to it will be called with the fulfillment value of that Promise. You can keep chaining Promises like this. If you need to handle any kind of exceptions you can add a catch(), as discussed in my previous article.

Now that you are aware of how Promise chaining works, we can move forward to see how asynchronous operations can be executed in order. But before that you need to understand few more things.

The resolve() and reject() Methods

The Promise API exposes several useful methods to make our lives easier. One of them is resolve(), which creates a new Promise object that always resolves. This means if you create a Promise with this technique and attach a then() to it, the success callback will always be called. You can also pass an argument to resolve() which becomes the fulfillment value of the Promise. If nothing is passed, the fulfillment value is undefined. Similarly, reject() creates a Promise object which always rejects. The following example shows how resolve() and reject() are used.

Promise.resolve('this always resolves').then(function(data) {
  alert(data); //this is called
});

Promise.reject('this always rejects').then(function(data) {
  alert(data); // this is never called
}).catch(function(err) {
  alert(err); //this is called
});

Enforcing Sequential Task Execution

Let’s create a simple application which accepts a list of movie titles and fetches a poster for each one. Here is the HTML markup, which shows an input field to enter comma separated movie titles:

<!DOCTYPE html>
<html>
  <head>
    <script src="script.js"></script>
  </head>
  <body>
    <input type="text" name="titles" id="titles" placeholder="comma separated movie titles" size="30"/>
    <input type="button" value="fetch" onclick="fetchMovies()" />
    <input type="button" value="clear" onclick="clearMovies()" />
    <div id="movies">
    </div>
  </body>
</html>

Now let’s use Promises to download a poster for each movie asynchronously. The following function creates a Promise and passes a callback to it which downloads movie information from a remote API.

function getMovie(title) {
  return new Promise(function(resolve, reject) {
    var request = new XMLHttpRequest();

    request.open('GET', 'http://mymovieapi.com/?q=' + title);
    request.onload = function() {
      if (request.status == 200) {
        resolve(request.response); // we get the data here, so resolve the Promise
      } else {
        reject(Error(request.statusText)); // if status is not 200 OK, reject.
      }
    };

    request.onerror = function() {
      reject(Error("Error fetching data.")); // error occurred, so reject the Promise
    };

    request.send(); // send the request
  });
}

The following snippet processes the downloaded information and updates the HTML page with the movie poster.

function fetchMovies() {
  var titles = document.getElementById('titles').value.split(',');

  for (var i in titles) {
    getMovie(titles[i]).then(function(data) {
      var img = JSON.parse(data)[0].poster.imdb;

      document.getElementById('movies').innerHTML = document.getElementById('movies').innerHTML + '<img src="' + img + '"/>';
    }).catch(function(error) {
      console.log(error);
    });
  }
}

The previous code is fairly self explanatory. It simply loops through the list of movie titles and extract the IMDB poster for each one. You can check out this Plunkr example to see the code in action.

But, there is a problem! In the Plunkr example enter some movie names separated by commas and click the fetch button. If you press fetch multiple times you will realize there is no particular order in which images are downloaded! Promises can be fulfilled in any order and therefore our images also come in a different order each time. So, this code won’t serve our purpose if we need to fetch movie posters in a particular order.

We can enforce ordering in two ways. First, we can create a Promise for a movie title only when the Promise for the previous title has been fulfilled. The second way involves creating a separate Promise that resolves only when the Promise for each movie title has been fulfilled and passes the fulfillment values in order.

Option 1

Have a look at the following snippet. We start by creating a Promise that always resolves. This is used to keep track of the previous Promise. Inside the loop, we call prevPromise.then() which returns a new Promise that we assign to prevPromise. This Promise is fulfilled when the Promise returned by getMovie(title) is fulfilled. So, the Promise for downloading a movie poster is created when the previous Promise (represented by prevPromise) is fulfilled. In this way we can download our images sequentially while still being asynchronous. Try this updated Plunkr. Every time you press fetch, the posters will be downloaded sequentially.

function fetchMovies() {
  var titles = document.getElementById('titles').value.split(',');
  var prevPromise = Promise.resolve(); // initial Promise always resolves

  titles.forEach(function(title) {  // loop through each title
    prevPromise = prevPromise.then(function() { // prevPromise changes in each iteration
      return getMovie(title); // return a new Promise
    }).then(function(data) {
      var img = JSON.parse(data)[0].poster.imdb;

      document.getElementById('movies').innerHTML = document.getElementById('movies').innerHTML + '<img src="' + img + '"/>';
    }).catch(function(error) {
      console.log(error);
    });
  });
}

Option 2

In the following code Promise.all() accepts an array of Promises and fulfills when all the Promises in the array fulfill. The fulfillment value of this Promise is the array of fulfillment values of each Promise, with the order maintained. So, once the Promise fulfills we can simply iterate over the array of data and extract the movie posters. Here is a Plunkr for this. Also note that in case of Promise.all() if any Promise in the array is rejected, the returned new Promise is rejected with that rejection value.

function fetchMovies() {
  var titles = document.getElementById('titles').value.split(',');
  var promises = [];

  for (var i in titles) {
    promises.push(getMovie(titles[i])); // push the Promises to our array
  }

  Promise.all(promises).then(function(dataArr) {
    dataArr.forEach(function(data) {
      var img = JSON.parse(data)[0].poster.imdb;

      document.getElementById('movies').innerHTML = document.getElementById('movies').innerHTML + '<img src="' + img + '"/>';
    });
  }).catch(function(err) {
    console.log(err);
  });
}

Conclusion

This article discussed some of the more advanced concepts of JavaScript Promises. Just make sure you have updated your browser to Chrome 32 beta or the latest Firefox nightly in order to run these code samples. It will take some time for browsers to implement these feature completely. Apart from that Promises are definitely the next big thing in JavaScript.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Taylor Ren

    This explains everything. Useful article!

  • Jingqi Xie

    catch used here?

  • James Simoes

    Bookmarked — thanks

  • Guest

    Nice article. :)

  • https://www.google.com/+SyedFazleRahman Syed Fazle Rahman

    Nice article Sandeep. Very informative. :)

  • Marian Vasile

    IMHO both solutions take us away from the parallel paradigm and force us to use promises in the old fashioned linear way. A much better approach would be to use an array watch and sort the results client side every time the array changes. Check out AngularJS for details.

  • LiangMingYi

    It’s great!

  • andrewstrader

    Thanks for the examples. They are helpful. But you could go even further and show just how useful promises really are. Just as a synchronous function is more versatile if it returns a value instead of setting a global variable, an asynchronous function is more versatile if it returns a promise object instead of calling external functions. That allows you to separate how requests are formed from how responses are handled. Then asynchronous workflows are not just chainable but also stackable, which further enhances code readability.