10 min read

JavaScript now has a new native pattern for writing asynchronous code called the Promise pattern. This new pattern removes the common code issues that the event and callback pattern had. It also makes the code look more like synchronous code. A promise (or a Promise object) represents an asynchronous operation.

Existing asynchronous JavaScript APIs are usually wrapped with promises, and the new JavaScript APIs are purely implemented using promises. Promises are new in JavaScript but are already present in many other programming languages. Programming languages, such as C# 5, C++ 11, Swift, Scala, and more are some examples that support promises.
In this tutorial, we will see how to use promises in JavaScript.

This article is an excerpt from the book, Learn ECMAScript – Second Edition, written by Mehul Mohan and Narayan Prusty.

Promise states

A promise is always in one of these states:

  • Fulfilled: If the resolve callback is invoked with a non-promise object as the argument or no argument, then we say that the promise is fulfilled
  • Rejected: If the rejecting callback is invoked or an exception occurs in the executor scope, then we say that the promise is rejected
  • Pending: If the resolve or reject callback is yet to be invoked, then we say that the promise is pending
  • Settled: A promise is said to be settled if it’s either fulfilled or rejected, but not pending

Once a promise is fulfilled or rejected, it cannot be transitioned back. An attempt to transition it will have no effect.

Promises versus callbacks

Suppose you wanted to perform three AJAX requests one after another. Here’s a dummy implementation of that in callback-style:

ajaxCall('http://example.com/page1', response1 => {
   ajaxCall('http://example.com/page2'+response1, response2 => {
     ajaxCall('http://example.com/page3'+response2, response3 => {
       console.log(response3)
     }
   })
})

You can see how quickly you can enter into something known as callback-hell. Multiple nesting makes code not only unreadable but also difficult to maintain. Furthermore, if you start processing data after every call, and the next call is based on a previous call’s response data, the complexity of the code will be unmatchable.

Callback-hell refers to multiple asynchronous functions nested inside each other’s callback functions. This makes code harder to read and maintain.

Promises can be used to flatten this code. Let’s take a look:

ajaxCallPromise('http://example.com/page1')
.then( response1 => ajaxCallPromise('http://example.com/page2'+response1) )
.then( response2 => ajaxCallPromise('http://example.com/page3'+response2) )
.then( response3 => console.log(response3) )

You can see the code complexity is suddenly reduced and the code looks much cleaner and readable. Let’s first see how ajaxCallPromise would’ve been implemented.

Please read the following explanation for more clarity of preceding code snippet.

Promise constructor and (resolve, reject) methods

To convert an existing callback type function to Promise, we have to use the Promise constructor. In the preceding example, ajaxCallPromise returns a Promise, which can be either resolved or rejected by the developer. Let’s see how to implement ajaxCallPromise:

const ajaxCallPromise = url => {
  return new Promise((resolve, reject) => {
    // DO YOUR ASYNC STUFF HERE
    $.ajaxAsyncWithNativeAPI(url, function(data) {
      if(data.resCode === 200) {
          resolve(data.message)
      } else {
          reject(data.error)
      }
    })
  })
}

Hang on! What just happened there?

  1. First, we returned Promise from the ajaxCallPromise function. That means whatever we do now will be a Promise.
  2. Promise accepts a function argument, with the function itself accepting two very special arguments, that is, resolve and reject.
  3. resolve and reject are themselves functions.
  4. When, inside a Promise constructor function body, you call resolve or reject, the promise acquires a resolved or rejected value that is unchangeable later on.
  5. We then make use of the native callback-based API and check if everything is OK. If everything is indeed OK, we resolve the Promise with the value being the message sent by the server (assuming a JSON response).
  6. If there was an error in the response, we reject the promise instead.

You can return a promise in a then call. When you do that, you can flatten the code instead of chaining promises again.For example, if foo() and bar() both return Promisethen, instead of:

foo().then( res => {
   bar().then( res2 => {
     console.log('Both done')
   })
})

We can write it as follows:

foo()
.then( res => bar() ) // bar() returns a Promise
.then( res => {
   console.log('Both done')
})

This flattens the code.

The then (onFulfilled, onRejected) method

The then() method of a Promise object lets us do a task after a Promise has been fulfilled or rejected. The task can also be another event-driven or callback-based asynchronous operation.

The then() method of a Promise object takes two arguments, that is, the onFulfilled and onRejected callbacks. The onFulfilled callback is executed if the Promise object was fulfilled, and the onRejected callback is executed if the Promise was rejected.

The onRejected callback is also executed if an exception is thrown in the scope of the executor. Therefore, it behaves like an exception handler, that is, it catches the exceptions.
The onFulfilled callback takes a parameter, that is, the fulfilment value of the promise. Similarly, the onRejected callback takes a parameter, that is, the reason for rejection:

ajaxCallPromise('http://example.com/page1').then( 
  successData => { console.log('Request was successful') },
  failData => { console.log('Request failed' + failData) } 
)

When we reject the promise inside the ajaxCallPromise definition, the second function will execute (failData one) instead of the first function.

Let’s take one more example by converting setTimeout() from a callback to a promise. This is how setTimeout() looks:

setTimeout( () => {
  // code here executes after TIME_DURATION milliseconds
}, TIME_DURATION)

A promised version will look something like the following:

const PsetTimeout = duration => {
   return new Promise((resolve, reject) => {
      setTimeout( () => {
         resolve()
      }, duration);
   })
}
// usage:

PsetTimeout(1000)
.then(() => {
console.log('Executes after a second')
})

Here we resolved the promise without a value. If you do that, it gets resolved with a value equal to undefined.

The catch (onRejected) method

The catch() method of a Promise object is used instead of the then() method when we use the then() method only to handle errors and exceptions. There is nothing special about how the catch() method works. It’s just that it makes the code much easier to read, as the word catch makes it more meaningful.

The catch() method just takes one argument, that is, the onRejected callback. The onRejected callback of the catch() method is invoked in the same way as the onRejected callback of the then() method.
The catch() method always returns a promise. Here is how a new Promise object is returned by the catch() method:

  • If there is no return statement in the onRejected callback, then a new fulfilled Promise is created internally and returned.
  • If we return a custom Promise, then it internally creates and returns a new Promise object. The new promise object resolves the custom promise object.
  • If we return something else other than a custom Promise in the onRejected callback, then a new Promise object is created internally and returned. The new Promise object resolves the returned value.
  • If we pass null instead of the onRejected callback or omit it, then a callback is created internally and used instead. The internally created onRejected callback returns a rejected Promise object. The reason for the rejection of the new Promise object is the same as the reason for the rejection of a parent Promise object.
  • If the Promise object to which catch() is called gets fulfilled, then the catch() method simply returns a new fulfilled promise object and ignores the onRejected callback. The fulfillment value of the new Promise object is the same as the fulfillment value of the parent Promise.

To understand the catch() method, consider this code:

ajaxPromiseCall('http://invalidURL.com')
.then(success => { console.log(success) },
failed => { console.log(failed) });

This code can be rewritten in this way using the catch() method:

ajaxPromiseCall('http://invalidURL.com')
.then(success => console.log(success))
.catch(failed => console.log(failed));

These two code snippets work more or less in the same way.

The Promise.resolve(value) method

The resolve() method of the Promise object takes a value and returns a Promise object that resolves the passed value. The resolve() method is basically used to convert a value to a Promise object. It is useful when you find yourself with a value that may or may not be a Promise, but you want to use it as a Promise. For example, jQuery promises have different interfaces from ES6 promises. Therefore, you can use the resolve() method to convert jQuery promises into ES6 promises.

Here is an example that demonstrates how to use the resolve() method:

const p1 = Promise.resolve(4);
p1.then(function(value){
  console.log(value);
}); //passed a promise object
Promise.resolve(p1).then(function(value){ 
console.log(value);
});

Promise.resolve({name: "Eden"})
.then(function(value){ 
console.log(value.name);
});

The output is as follows:

4
4
Eden

The Promise.reject(value) method

The reject() method of the Promise object takes a value and returns a rejected Promise object with the passed value as the reason. Unlike the Promise.resolve() method, the reject() method is used for debugging purposes and not for converting values into promises.

Here is an example that demonstrates how to use the reject() method:

const p1 = Promise.reject(4);
p1.then(null, function(value){
console.log(value);
});
Promise.reject({name: "Eden"})
.then(null, function(value){
console.log(value.name);
});

The output is as follows:

4
Eden

The Promise.all(iterable) method

The all() method of the Promise object takes an iterable object as an argument and returns a Promise that fulfills when all of the promises in the iterable object have been fulfilled.

This can be useful when we want to execute a task after some asynchronous operations have finished. Here is a code example that demonstrates how to use the Promise.all() method:

const p1 = new Promise(function(resolve, reject){
 setTimeout(function(){
  resolve();
 }, 1000);
});
const p2 = new Promise(function(resolve, reject){
setTimeout(function(){
resolve();
}, 2000);
});

const arr = [p1, p2];
Promise.all(arr).then(function(){
console.log("Done"); //"Done" is logged after 2 seconds
});

If the iterable object contains a value that is not a Promise object, then it’s converted to the Promise object using the Promise.resolve() method.

If any of the passed promises get rejected, then the Promise.all() method immediately returns a new rejected Promise for the same reason as the rejected passed Promise. Here is an example to demonstrate this:

const p1 = new Promise(function(resolve, reject){
 setTimeout(function(){
  reject("Error");
 }, 1000);
});
const p2 = new Promise(function(resolve, reject){
setTimeout(function(){
resolve();
}, 2000);
});

const arr = [p1, p2];
Promise.all(arr).then(null, function(reason){
console.log(reason); //"Error" is logged after 1 second
});

The Promise.race(iterable) method

The race() method of the Promise object takes an iterable object as the argument and returns a Promise that fulfills or rejects as soon as one of the promises in the iterable object is fulfilled or rejected, with the fulfillment value or reason from that Promise.

As the name suggests, the race() method is used to race between promises and see which one finishes first. Here is a code example that shows how to use the race() method:

var p1 = new Promise(function(resolve, reject){ 
setTimeout(function(){ 
resolve("Fulfillment Value 1"); 
}, 1000);
});
var p2 = new Promise(function(resolve, reject){ 
setTimeout(function(){
resolve("fulfillment Value 2"); 
}, 2000);
});
var arr = [p1, p2];
Promise.race(arr).then(function(value){ 
console.log(value); //Output "Fulfillment value 1"
}, function(reason){ 
console.log(reason);
});

Now at this point, I assume you have a basic understanding of how promises work, what they are, and how to convert a callback-like API into a promised API. Let’s take a look at async/await, the future of asynchronous programming.

If you found this article useful, do check out the book Learn ECMAScript, Second Edition for learning the ECMAScript standards to design your web applications.

Read next

Publishing Product Manager interested in learning how emerging technologies are making the world a better place | Still learning to write better and read more.

LEAVE A REPLY

Please enter your comment!
Please enter your name here