JavaScript ES6 Promises

A Promise provides an easy way to work with asynchronous programming in JavaScript. In this tutorial, we'll explain what asynchronous programming is and how the Promise constructor makes async programming more elegant. We'll also provide an example of creating a Promise object and working with promise chaining.

What is asynchronous programming?

Asynchronous programming involves running a process separately from a main thread and notifying the main thread when it completes. This concept is better explained through a real world scenario:

Let's say your web app returns a list of items when a button is clicked. When the user clicks the button, an HTTP GET request is made to a server to retrieve the list of items. This means the user waits for network communication between the client and the server to take place before he/she sees the list of items.

When the HTTP request is made asynchronously the user can still navigate the web app while the information loads. If the HTTP request is made synchronously, the user has to wait for the response to continue using the app.

This is the key advantage of asynchronous programming. It allows you to run potentially blocking I/O operations in the background without interrupting a main thread or process.

How does a Promise work?

The Promise objects represents the eventual completion of an asynchronous operation. It returns a single value based on an operation being resolved or rejected. A promise is always in one of three stages:

The Promise() constructor takes two arguments: a resolve function and a reject function. It returns one or the other based on the outcome of the asynchronous operation.

Creating a Promise

You can create a promise in JavaScript using the Promise constructor:

const myPromise = amount => {
  return new Promise((resolve,reject) => {
    if(amount > 0){
      resolve("success!")
    }
    reject("failure!")
  })
}

myPromise(1)
//resolves Promise { 'success!' }

In the example above, we create a myPromise() function that takes a single amount parameter and returns a Promise object. The promise constructor function takes two arguments: resolve and reject.

We then call our myPromise(1) function. Notice how it returns a resolved Promise object with the "success!" message since our argument 1 is greater than 0.

If we hadn't wrapped our Promise in a function, then it would have executed when it was defined rather than when we invoked it via myPromise(1).

Promise Methods

Promise methods exist to handle the resolution or rejection of a Promise object. Below is a brief description and example of the methods commonly used with JavaScript promises:

then()

The then() method executes after a promise is either fulfilled or rejected. It takes two function arguments for resolved and rejected.

let handleSuccess = (x) => {
  console.log(x + " it worked!")
}

let handleError = (x) => {
  console.log(x + " oh no, it failed!")
}

const myPromise = amount => {
  return new Promise((resolve,reject) => {
    if(amount > 0){
      resolve("success!")
    }
    reject("failure!")
  })
}

myPromise(1).then(handleSuccess, handleError)

//logs 'success! it worked!'

myPromise(0).then(handleSuccess,handleError)

//logs 'failure! oh no, it failed!'

Notice how we call then() twice on our constructed myPromise() function. It's important to remember that the method accepts two functions as arguments, in our case handleSuccess() and handleError(). Notice how the original message gets passed to the handler function for both resolved and rejected scenarios.

catch()

The catch() method provides a better way to handle rejections and failures. It is a "catch all" for any rejected promise:

const myPromise = amount => {
  return new Promise((resolve,reject) => {
    if(amount > 0){
      resolve("success!")
    }
    reject("failure!")
  })
}

myPromise(0).then(res => {
  console.log(res + " success!")
}).catch(err => {
  console.log(err + "oh no, it failed!")
})

//logs 'failure! oh no, it failed!'

In the above example, catch() takes the returned promise from the then() function and handles the rejection with its own argument function. Notice how we only pass a single argument to the then() function for handling a successful response.

Not only does the catch() method save us from having to specify the second argument for then(), it also catches any other errors along the way. Any internal errors thrown by then() will still be caught by catch():

const myPromise = amount => {
  return new Promise((resolve,reject) => {
    if(amount > 0){
      resolve("success!")
    }
    reject("failure!")
  })
}

myPromise(1).then(res => {
  throw new Error();
  console.log(res + " success!")
}).catch(err => {
  console.log(err + "oh no, it failed!")
})

//logs 'Error oh no, it failed!'

Even if we throw an error within our then() handler, the catch() method will still catch it!

Promise.resolve()

Returns a resolved promise with the given value:

Promise.resolve("Success")

//returns Promise {'Success'}

Promise.reject()

Returns a rejected promise with the given value:

Promise.reject("error")

//returns unhandled promise rejection

Promise.all()

The all() method takes an array of promises as an argument. It returns a resolved promise based on the referenced promises ALL being fulfilled or rejected:

const p1 = new Promise((resolve,reject) => {
    setTimeout(resolve("p1 success"),2000)
})

const p2= new Promise((resolve,reject) =>{
    setTimeout(resolve("p2 success"),4000)
})

Promise.all([p1,p2]).then(res => {
    console.log(res);
})

//logs ['p1 success', 'p2 success'] after 4 seconds

Promise.race()

The race() method takes an array of promises as an arugment. It returns a resolved promise based on the first referenced promise that resolves.

const p1 = new Promise((resolve,reject) => {
    setTimeout(resolve("p1 success"),2000)
})

const p2= new Promise((resolve,reject) =>{
    setTimeout(resolve("p2 success"),4000)
})

Promise.race([p1,p2]).then(res => {
    console.log(res);
})

//logs 'p1 success' since p1 finishes first

Since p1 is the first to resolve, race() returns the resolved promise for p1 .

ES6 Promise Chaining

You'll notice we call then() and catch() with dot notation. This is called chaining and it works because we return a resolved promise for each chained method.

Chaining can be a powerful tool. As long as you are returning a promise, you can extend async operations indefinitely.

Conclusion

Promises make working with async activity easier. While having to create your own Promise objects isn't a common requirement, understanding how they work will give you a more sophisticated understanding of asynchronous programming with ES6 JavaScript.

Join the conversation...