If you've ever seen anything that looks like this:

p.then(function(result) {
  // do something with the result
})

then you're already one step closer to understanding promises. If you haven't, fear not, we'll be going through what promises are and how you use them with lots of examples in this tutorial.

As with anything new, some people find promises a bit difficult to wrap their head around because it involves a different way of thinking about your code. But trust me, once you see some examples, you'll appreciate their elegance, especially when you begin applying them to your own code. They make writing and thinking about complex asynchronous code easier and help you avoid multiple levels of nested callbacks.

The main idea behind JavaScript promises is:

A promise represents the eventual result of an asynchronous operation

What that basically means is that you wont be able to access the result of that async operation immediately. Instead, you have to wait until the promise has been settled.

Think of it like any promise you would make in your day-to-day life. For example, when you were younger and told your mom "I promise to take out the trash". That meant you will fulfill your promise and throw out the trash some time later in the day. That could be in a couple of minutes or a couple of hours.

That's the same idea behind JavaScript promises. When a function returns a promise, it's essentially saying: "here's my promise that I'll get back to you with the result as soon as I'm done working on it".

Promises By Example

Rather than boring you a bunch of theory and terminology, let's jump straight into an example and relate that back to the theory as we go along.

At some point in time, you've made an AJAX request using JavaScript. You probably used the plain, old XMLHttpRequest, jQuery's $.ajax(), or some other library.

It's no secret that the XMLHttpRequest API is complex and difficult to remember. Thankfully, the new fetch API will make AJAX requests a breeze! And guess what? It uses promises to handle results!

So what does it mean when I say that the fetch API uses promises? Basically, whenever you make a request with fetch(), like so:

fetch('/some/endpoint')

it will return a Promise object.

Traditionally, we would pass a callback function which would be triggered when an async request is complete. However, to interact with a promise you use the then() method.

You can chain a .then(onFulfilled, onRejected) method to access the result of our fetch call:

fetch('/some/endpoint').then(function (result) {
  console.log('Response from the endpoint ' + result)
}, function (error){
  console.log('An error occurred: ' + error)
})

The then method allows us to access the result of the call by providing 2 optional arguments:

  1. onFulfilled - called when a promise is fulfilled (success), with the promise's value as it's first argument
  2. onRejected - called when a promise is rejected (failure), with the promise's reason as it's first argument

As you may have noticed, I've introduced 2 new terms, fulfilled and rejected, which are referred to as states. There is also a third state which a promise can be in: pending. So let's talk about what those mean in the context of our example.

  • As soon as we call fetch(), it will return a promise, as we mentioned earlier
  • The promise will be in a pending state until the request has completed
  • From the pending state, a promise can transition into a fulfilled state or a rejected state

It's worth noting that once a promise transitions to a fulfilled or rejected state, it's value will never change

Error Handling with catch()

Conveniently, the Promises/A+ Specification provides us with some syntactic sugar with which we can handle errors (rejections) from a promise.

Assume we had a function called lookupPrice(itemId) that returns a promise which succeeds (resolves) if it finds the item's price in the database, given that itemId, or fails (rejects) if it does not. Using the catch() method we can write it is a nice and readable way:

lookupPrice(123)
  .then(function (price) {
    console.log('The item costs: ' + price)
  })
  .catch(function (error){
    console.log('Could not find an item with that ID')
  })

For example, this will print "The item costs: 50" if the promise is resolved successfully or it will print "Could not find an item with that ID" if it is rejected due to an error.

If you prefer writing it using only the then() method, the above snippet would be equivalent to chaining 2 then() methods and omitting the onFulfilled or onRejected handler like so:

lookupPrice(123)
  .then(function (price) {
    console.log('The item costs: ' + price)
  }, null)
  .then(null, function (error){
    console.log('Could not find an item with that ID')
  })

If a promise is rejected, the execution will skip to the first catch or then method with a onRejected handler in the chain.

This means that if we were calling lookupPrice() with an ID of an item that does not exist, like so:

lookupPrice(99999)
  .then(function (price) {
    console.log('The item costs: ' + price)
    return price
  })
  .then(function (price) {
    console.log('Double the price: ' + (price * 2))
  })
  .catch(function (error){
    console.log('Could not find an item with that ID')
  })

both the then() methods will be skipped (because they don't have an onRejected handler) and the catch will be executed.

There is a subtle difference between the 2 snippets below, pay close attention:

// snippet 1 - using then(onFulfilled, onRejected)
lookupPrice(123)
  .then(function (price) {
    // lookup another price and return the promise created by lookupPrice(456)
    return lookupPrice(456)
  }, function (error){
    console.log('Item 123 does not exist')
  })

and

// snippet 2 - using .catch()
lookupPrice(123)
  .then(function (price) {
    // lookup another price and return the promise created by lookupPrice(456)
    return lookupPrice(456)
  })
  .catch(function (error){
    console.log('Either item 123 OR 456 does not exist')
  })

In snippet 1, if item 123 does not exist, the next then() with an onRejected handler will be executed immediately:

  • In this case we will see the output "Item 123 does not exist"
  • However, there are no then() methods with an onRejected handler or catch() methods to handle lookupPrice(456), if it rejects. So if item 456 does not exist, then nothing will be printed. It will be an Uncaught error

The important point to note is that when you have a then(onFulfilled, onRejected), either onFulfilled or onRejected will be executed, but not both.

In snippet 2:

  • If item 123 does not exist, the next catch will be executed, printing "Either item 123 OR 456 does not exist"
  • Also, since there is a catch() method chained to the then() method, any rejections that occur within the second then() will be handled by the catch as well. So in this case, if item 456 does not exist, we will also see the message: "Either item 123 OR 456 does not exist"

Creating a Promise with new Promise()

Up until now we've looked at the fetch() API which returns a promise. From there, we used the then() method to interact with the promise's result. In most cases, you will be using libraries that do all sorts of different things and return a promise. However, there will be times when you need to create your own promise and determine what causes it to resolve and what causes it to be rejected.

To create a promise you use the Promise(resolver) constructor, where resolver is a method which takes 2 arguments: resolve and reject.

As an example, say we wanted to create a function that generates a random number between 1 and 10, and returns a promise. If the number is 5 or less, then we want to resolve the promise (meaning the operation was successful). If the number is 6 or more, we want to reject the promise (the operation was unsuccessful).

function generateRandomNumber () {
  return new Promise(function (resolve, reject) {
    var randomNumber = Math.floor((Math.random() * 10) + 1)
    if (randomNumber <= 5) {
      resolve(randomNumber)
    } else {
      reject(randomNumber)
    }
  })
}

generateRandomNumber().then(function(result) {
  console.log('Success: ' + result)
}).catch(function(error) {
  console.log('Error: ' + error)
})

Here's a Fiddle that you can play around with:

The generateRandomNumber() function returns a promise using the Promise constructor. The resolver callback we pass to our Promise constructor receives a resolve and reject method as arguments.

If the randomNumber turns out to be less than or equal to 5, we consider this a success and hence call the resolve method. You can pass the result of the promise, in our case the random number less than or equal to 5, to the resolve method, which can then be accessed using the then() method.

Similarly, if the number is greater than 5, we consider it a failure and call the reject method with that number. This will cause the catch() method to be executed with that number as an argument, which is referred to as the reason for rejection.

Typically you would be performing asynchronous operations in the resolver callback and resolving/rejecting the promise as opposed to performing synchronous tasks.

Chaining - One Step at a Time

Chaining multiple then() methods allows you to further transform values or perform additional async tasks in sequence!

This is really useful when, say, you want to call an endpoint to authenticate a user, and if the user was successfully authenticated, we want to call another endpoint to fetch their profile information.

If we had 2 hypothetical endpoints, the above scenario would look something like this:

// try to authenticate a user with an id=333
fetch('/auth/333')
   // pass response object from the fetch call to authStatus
   // to check if authentication succeeded/failed
  .then(authStatus)
  // if authentication succeeded, call loadProfile() with the userName
  .then(loadProfile)
  // if authentication failed, we catch the rejected promise
  .catch(function (error) {
    console.log(error)
  })

function authStatus (response) {
  // if the server authenticates the user successfully, get the userName
  // from the response and resolve the promise
  if (response.status === 200) {
    // response.userName will be passed to the next `then`,
    // in this case it's the loadProfile function
    return Promise.resolve(response.userName)
  } else {
    // authentication failed, reject the promise
    return Promise.reject('User cannot be authenticated')
  }
}

function loadProfile (userName) {
  // fetch and log user's profile info with the userName passed in
  // from the authStatus function
  fetch('/profile/' + userName)
    .then(function (response) {
      console.log(response.profileData)
    })
}

Take a minute or two to read over that example a couple of times to see how everything links together.

There are 2 important things to note in the above example:

  • When a promise if resolved, it's value is automatically passed as an argument to the next then() method
  • If the promise is rejected in authStatus, the catch method will be executed immediately and .then(loadProfile) will be skipped

Promise.all() - All at Once

We talked about chaining to help us transform the result of an async operation or even perform additional async operations in sequence. What if we don't care about performing async operations in sequence? In other words, say we wanted to grab a user's profile information and all their recent blog posts simultaneously.

That's when Promise.all() comes in handy. It takes an array of promises as an argument and creates a promise that is resolved when all of promises have successfully been resolved. It's important to remember that Promise.all() will reject as soon as any of the promises reject. Basically, either all of the promises succeed, or none of them do.

The value/result of the promise returned from Promise.all() when it is successfully resolved is an array of results corresponding to the promises you passed in. Going back to our use case of loading a user's profile info and recent blog posts, we can use Promise.all() to fetch these items concurrently. Putting it into code will look something like this:

Promise.all([
  fetch('/profile/333'), // get user's profile info
  fetch('/posts/user/333') // get posts by that user
])
.then(function (result) {
  // `result` will be an array with the values returned by
  // the 2 fetch calls in the same order they were passed in
  var profileInfo = result[0].profileData
  var recentPosts = result[1].posts

  // do something with that data
})

That's All, I Promise

You'll find that promises are quite simple once you actually start applying them in practice. So I'd encourage you to open up your developer tools in Chrome/Firefox and try out some of the stuff you learned in this tutorial.

I should point out that there's actually one last thing in the Promise API I didn't cover and it's Promise.race(). It takes an array of promises like Promise.all(), but instead, it resolves as soon as any of the promises resolve or rejects as soon as any of the promises reject. Personally, I think that it's not entirely useful, but I'm sure there are some specific scenarios which could make use of it.