Author avatar

Chris Parker

How to Implement ES6 Promises in Your App

Chris Parker

  • Feb 26, 2019
  • 8 Min read
  • 105 Views
  • Feb 26, 2019
  • 8 Min read
  • 105 Views
React
ES6

Introduction

Promises are a very important feature added to ES6. The introduction of promises has helped simplify async programming by making the code easier to understand and, also, by eliminating the issues related to callbacks.

In this guide, we'll see what these ES6 promises are and how we can use them within our applications. One of the most-common examples of using promises in your application would be to make a database request or API calls and you have to update some DOM element on the UI once you receive the response from the database.

What Is a Promise?

Let's start with the basic definition of a Promise. A Promise is an object that can be used as a placeholder for the eventual result of a deferred computation. Thus, it can be said to be a container for a future value.

That is all you need to remember about the definition of Promises.

Creating a New Promise

To create a new Promise in our application, we have to use the Promise constructor.

1
2
3
4
5
6
const myNewPromise = new Promise((resolve, reject) => {
    if (Math.random() * 100 == 100) {
        resolve('Wow, You hit a century!');
    }
    reject(new Error('Oops, You missed hitting century !'));
});
javascript

If you have a look at the above code, you'll observe that we are not really writing actual code to be executed inside this function (e.g. updating UI on the page once the promise is resolved). Instead, we handle this in a more lean way.

What we'll do is make use of the "resolve" param, which was passed into the Promise. Also, this parameter is actually a function and we can pass an argument back to resolve. This return value would represent what we want to pass back out of the promise, once it has been resolved.

Using Promises

To use the above Promise which was created, we can make use of the then() method. The then() method takes two parameters/callbacks.- The first callback gets invoked when our promise is resolved (e.g. this can be when an AJAX call has finished and we get some response back from our backend). The second callback gets invoked when our promise gets rejected (e.g. our AJAX call failed because of network connectivity issues).

So we can simply use:

1
2
3
4
5
myNewPromise.then((resolvedValue) => {
    console.log(resolvedValue); // Logs 'Wow, You hit a century!'
}, (errorValue) => {
    console.log(errorValue); // Logs 'Oops, You missed hitting century !'
});
javascript

There is a shorthand way of writing this code as well:

1
2
3
4
const onResolved = (resolvedValue) => console.log(resolvedValue);
const onRejected = (errorValue) => console.log(errorValue);

myPromise.then(onResolved, onRejected);
javascript

Error/Exception Handling

Ideally, we should use catch() for handling errors, instead of using promise.then(resolveFn, rejectFn).

Also, catch() is a much more explicit way for handling errors than using the reject function. When we want to chain promises, we can have a single catch() at the end of our chain to handle any exceptions which might arise out of our initial promises or any of our handlers.

Below is an example of using catch()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var promise1 = new Promise((resolve, reject) => {  
  if (true) {  
    throw new Error("Rejected!"); // same as rejected
	}  else {
    resolve(100);
}
});

// trailing .catch() handles rejection
promise1.then((val) => console.log("Successful!"))  
 .then((val) => console.log("Returned value is : ", val))
 .catch((error) => console.log("error: ", error.message));

// error: Rejected!
javascript

Composing Promises

When working with multiple promises, we might run into a scenario where we want to start our processing when all of them are finished. For example, say we have a grid with dynamic columns based on user preference and dynamic data for those columns. So, we'll have two API calls - one to fetch those dynamic columns and another which gets use the actual data. Thus, we'll be able to map the grid data/body to those columns only when we have responses for both these API calls from the server.

This is where Promise.all can help. It would take an array of Promises, which resolve only after all of them are successful, to code the above scenario.

Instead, we can handle the it in the following way:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var getJSONFromServer = function(url) {  
  return new Promise((resolve, reject) => {
    $.getJSON(url)
      .done((json) => resolve(json))
      .fail((xhr, status, err) => reject(status + err.message));
  });
}

var gridUrls = {  
    'http://www.someapi.com/getGridColumns',
    'http://www.someapi.com/getGridData'
  },
  itemPromises = gridUrls.map(getJSONFromServer);

Promise.all(itemPromises)  
  .then(function(results) {
     // We reach here only if ALL promises are successful
     results.forEach(function(item) {
       // Render the grid
     });
  })
  .catch(function(error) {
    // Catch failure of first failed promise
    console.log("Failed: ", error);
  });
javascript

With Promises.all(), any exception in any of the promises passed to Promise.all() will cause the Promise to be rejected.

In certain cases, we don't need to wait for all the Promises in our array but we want to get the response from the first promise in the array to be successful. For this, we can use Promise.race(). This would also take an array of promises but, unlike Promise.all(), race() will fulfill as soon as the first promise within that array is fulfilled.

Promise.all() has what is called as fail-fast behavior. This means that if any promise is rejected from the array of promises that were passed, the resulting promise of Promise.all() will be rejected immediately. Thus, it will not really wait for the other promises to finish and we receive only the error for the rejected request.

Promise.all Example

Below is another example of using Promise.all. Using the output logs, you can clearly see the sequence of execution for each of the Promises.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function DoThisAsyncTask(delayParam) {
    return new Promise(function(resolve) {
        setTimeout(function() {
            console.log('Resolving promise with delay : ', delayParam);
            resolve('Done ' + delayParam);
        }, delayParam);
    });
}

var promise = Promise.all([DoThisAsyncTask(1500), DoThisAsyncTask(2500), DoThisAsyncTask(500), DoThisAsyncTask(3500)]);

promise.then(function(data) {
    console.log('All promises resolved!');
    data.forEach(function(text) {
        console.log(text);
    });
});

//Output
//Resolving promise with delay :  500
//Resolving promise with delay :  1500
//Resolving promise with delay :  2500
//Resolving promise with delay :  3500
//All promises resolved!
//Done 1500
//Done 2500
//Done 500
//Done 3500
javascript

Promise States

A Promise is always in one of the following three mutually exclusive states at any given time: 1. Before the result is available, the Promise is said to be pending (e.g. waiting for the server to respond back with JSON response). 2. If the result is available, the Promise is said to be fulfilled (e.g. server gave the response to the browser). 3. If any error/exception occurs, the Promise is said to be rejected (e.g. AJAX call failed because of network connectivity issues).

A Promise is settled only if everything is completed (i.e. if it is either successful or rejected).

A Promise is settled only once.

Conclusion

Adding native support for Promises has been a very nice addition to ES6. This is going to be really helpful in apps which implement a lot of async features. It improves the readability of code compared to using callback handlers. Also, with promises, it is much easier to implement error handling.

4