🤯 New: Coding Assessments Practice your skills on real-world programming challenges

An Overview of JavaScript Promises

Sandeep Panda, James Hibbard
Share

In this tutorial, we’ll learn how to create and work with promises in JavaScript. We’ll look at chaining promises, error handling, and some of the more recent promise methods added to the language.

What Are JavaScript Promises?

In JavaScript, some operations are asynchronous. This means that the result or value they produce isn’t immediately available when the operation completes.

A promise is a special JavaScript object which represents the eventual result of such an asynchronous operation. It acts like a proxy to the operation’s result.

The Bad Old Days: Callback Functions

Before we had JavaScript promises, the preferred way of dealing with an asynchronous operation was to use a callback. A callback is a function that’s run when the result of the asynchronous operation is ready. For example:

setTimeout(function() {
  console.log('Hello, World!');
}, 1000);

Here, setTimeout is an asynchronous function that runs whichever callback function it’s passed after a specified number of milliseconds. In this case, it logs “Hello, World!” to the console after one second has passed.

Now imagine we wanted to log a message every second for five seconds. That would look like this:

setTimeout(function() {
  console.log(1);
  setTimeout(function() {
    console.log(2);
    setTimeout(function() {
      console.log(3);
      setTimeout(function() {
        console.log(4);
        setTimeout(function() {
          console.log(5);
        }, 1000);
      }, 1000);
    }, 1000);
  }, 1000);
}, 1000);

Asynchronous JavaScript that uses multiple nested callbacks in this way is both error prone and hard to maintain. It’s often referred to as callback hell, and even has its own web page.

Admittedly, this is a contrived example, but it serves to illustrate the point. In a real-world scenario, we might make an Ajax call, update the DOM with the result, then wait for an animation to complete. Or, our server might receive input from the client, validate that input, update the database, write to a log file and finally send a response. In both cases, we’d also need to handle any errors which occur.

Using nested callbacks to accomplish such tasks would be painful. Luckily, promises provide us with a much cleaner syntax that enables us to chain asynchronous commands so they run one after another.

How to Create a JavaScript Promise Object

The basic syntax for creating a promise is as follows:

const promise = new Promise((resolve, reject) => {
  //asynchronous code goes here
});

We start by instantiating a new promise object using the Promise constructor and passing it a callback function. The callback takes two arguments, resolve and reject, which are both functions. All of our asynchronous code goes inside that callback.

If everything runs successfully, the promise is fulfilled by calling resolve. In case of an error, the promise is rejected by calling reject. We can pass values to both methods which will then be available in the consuming code.

To see how this works in practice, consider the following code. This makes an asynchronous request to a web service that returns a random dad joke in JSON format:

const promise = new Promise((resolve, reject) => {
  const request = new XMLHttpRequest();
  request.open('GET', 'https://icanhazdadjoke.com/');
  request.setRequestHeader('Accept', 'application/json');

  request.onload = () => {
    if (request.status === 200) {
      resolve(request.response); // we got data here, so resolve the Promise
    } else {
      reject(Error(request.statusText)); // status is not 200 OK, so reject
    }
  };

  request.onerror = () => {
    reject(Error('Error fetching data.')); // error occurred, reject the  Promise
  };

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

The Promise Constructor

We start off by using the Promise constructor to create a new promise object. The constructor is used to wrap functions or APIs that don’t already support promises, such as the XMLHttpRequest object above. The callback passed to the promise constructor contains the asynchronous code used to get data the from remote service. (Note that we’re using an arrow function here.) Inside the callback, we create an Ajax request to https://icanhazdadjoke.com/, which returns a random dad joke in JSON format.

When a successful response is received from the remote server, it’s passed to the resolve method. In case of any error happening — either on the server or at a network level — reject is called with an Error object.

The then Method

When we instantiate a promise object, we get a proxy to the data that will be available in future. In our case, we’re expecting some data to be returned from the remote service. So, how do we know when the data becomes available? This is where the Promise.then() function is used:

const promise = new Promise((resolve, reject) => { ... });

promise.then((data) => {
  console.log('Got data! Promise fulfilled.');
  document.body.textContent = JSON.parse(data).joke;
}, (error) => {
  console.error('Promise rejected.');
  console.error(error.message);
});

This function can take two arguments: a success callback, and a failure callback. These callbacks are called when the promise is settled (that is, either fulfilled or rejected). If the promise was fulfilled, the success callback will be fired with the actual data we passed to resolve. If the promise was rejected, the failure callback will be called. Whatever we passed to reject will be passed as an argument to this callback.

We can try this code out in the CodePen demo below. To view a new random joke, hit the RERUN button in the bottom right-hand corner of the embed.

See the Pen
An Overview of JavaScript Promises (1)
by SitePoint (@SitePoint)
on CodePen.

What are the states of a JavaScript promise?

In the code above, we saw that we can change the state of the promise by calling the resolve or reject methods. Before we move on, let’s take a second to look at the lifecycle of a promise.

A promise may be in one of these states:

  • pending
  • fulfilled
  • rejected
  • settled

A promise starts life in a pending state. That means that it has been neither fulfilled nor rejected. If the action relating to the promise is successful (a remote API call in our case) and the resolve method is called, the promise is said to be fulfilled. If, on the other hand, the related action is unsuccessful and the reject method is called, the promise is in a rejected state. Finally, a promise is said to be settled if it’s either fulfilled or rejected, but not pending.

The four states of a JavaScript promise

Once a promise is rejected or fulfilled, this status gets permanently associated with it. This means a promise can succeed or fail only once. If the promise has already been fulfilled and later we attach a then() to it with two callbacks, the success callback will be correctly called. So, in the world of promises, we’re not interested in knowing when the promise is settled. We’re only concerned with the final outcome of the promise.

But shouldn’t we be using the Fetch API?

At this point, we might be asking why we’re not using the Fetch API to fetch the data from the remote server, and the answer is that we probably should be.

Unlike the XMLHttpRequest object, the Fetch API is promise-based, which means we could rewrite our code like so (minus error handling):

fetch('https://icanhazdadjoke.com', { 
  headers: { 'Accept': 'application/json' }
})
  .then(res => res.json())
  .then(json => console.log(json.joke));

The reason for using XMLHttpRequest was to give more of an insight into what’s going on under the hood.

Chaining Promises

It might sometimes be desirable to chain multiple asynchronous tasks together in a specific order. This is known as promise chaining. Let’s revisit our setTimeout example to get a basic idea of how promise chaining works.

We could start by creating a new promise object as we did previously:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => { resolve() }, 1000)
});

promise.then(() => {
  console.log(1);
});

As expected, the promise is resolved after a second has passed and “1” is logged to the console.

To continue the chain, we need to return a second promise after our console statement and pass it to a second then:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => { resolve() }, 1000)
});

promise.then(() => {
  console.log(1);
  return new Promise((resolve, reject) => {
    setTimeout(() => { resolve() }, 1000)
  });
}).then(() => {
  console.log(2);
});

And while this works, it’s already starting to get a bit unwieldy. Let’s make a function that returns a new promise, which is resolved after a specific time has elapsed:

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

We can then use that to flatten our nested code:

sleep(1000)
  .then(() => {
    console.log(1);
    return sleep(1000);
  }).then(() => {
    console.log(2);
    return sleep(1000);
  }).then(() => {
    console.log(3);
    return sleep(1000);
  })
  ...

And because the then method itself returns a promise object and we aren’t passing any values from one asynchronous operation to the next, this allows us to further simplify things:

sleep(1000)
  .then(() => console.log(1))
  .then(() => sleep(1000))
  .then(() => console.log(2))
  .then(() => sleep(1000))
  .then(() => console.log(3))
  ...

This is much more elegant than the original code.

Please note that, if you’d like to learn more about implementing a sleep function in JavaScript, you might find this interesting: Delay, Sleep, Pause, & Wait in JavaScript.

Passing data down the promise chain

When we have multiple asynchronous operations to be performed, we’ll likely want to pass the result of one asynchronous call to the next then block in the promise chain, so that we can do something with that data.

For example, we might want to fetch a list of contributors to a GitHub repository, then use this information to get the name of the first contributor:

fetch('https://api.github.com/repos/eslint/eslint/contributors')
  .then(res => res.json())
  .then(json => {
    const firstContributor = json[0].login;
    return fetch(`https://api.github.com/users/${firstContributor}`)
  })
  .then(res => res.json())
  .then(json => console.log(`The first contributor to ESLint was ${json.name}`));

// The first contributor to ESLint was Nicholas C. Zakas

As we can see, by returning the promise returned from the second fetch call, the server’s response (res) is available in the following then block.

Promise Error Handling

We have already seen that the then function takes two callback functions as arguments and that the second one will be called if the promise was rejected:

promise.then((data) => {
  console.log('Got data! Promise fulfilled.');
  ...
}, (error) => {
  console.error('Promise rejected.');
  console.error(error.message);
});

However, specifying an error handler for each promise could get quite verbose when dealing with promise chains. Luckily, there’s a better way…

The catch Method

We can also use the catch method, which can handle errors for us. When a promise rejects anywhere in a promise chain, control jumps to the closest rejection handler. This is very handy, as it means we can add a catch onto the end of the chain and have it deal with any errors that occur.

Let’s take the previous code as an example:

fetch('https://api.github.com/repos/eslint/eslint/contributors')
  .then(res => res.json())
  .then(json => {
    const firstContributor = json[0].login;
    return fetch(`https://api.github.com/users/${firstContributor}`)
  })
  .then(res => res.jsn())
  .then(json => console.log(`The top contributor to ESLint wass ${json.name}`))
  .catch(error => console.log(error));

Notice that, in addition to adding an error handler at the end of the code block, I’ve misspelled res.json() as res.jsn on the seventh line.

Now when we run the code, we see the following output to the screen:

TypeError: res.jsn is not a function
  <anonymous>  http://0.0.0.0:8000/index.js:7  
  promise callback*  http://0.0.0.0:8000/index.js:7  

index.js:9:27

The file that I’m working in is called index.js. Line 7 contains the mistake and line 9 is the catch block which caught the error.

The finally method

The Promise.finally method is run when the promise is settled — that is, either resolved or rejected. Like catch, it helps prevent code duplication and is quite useful for performing clean-up tasks, such as closing a database connection, or removing a loading spinner from the UI.

Here’s an example using our previous code:

function getFirstContributor(org, repo) {
  showLoadingSpinner();
  fetch(`https://api.github.com/repos/${org}/${repo}/contributors`)
  .then(res => res.json())
  .then(json => {
    const firstContributor = json[0].login;
    return fetch(`https://api.github.com/users/${firstContributor}`)
  })
  .then(res => res.json())
  .then(json => console.log(`The first contributor to ${repo} was ${json.name}`))
  .catch(error => console.log(error))
  .finally(() => hideLoadingSpinner());
};

getFirstContributor('facebook', 'react');

It doesn’t receive any arguments and returns a promise, so that we can chain more then, catch, and finally calls onto its return value.

Further Promise Methods

By this point, we’ve got a good basic understanding of working with JavaScript promises, but before we wrap up, there are various promise utility methods to be aware of.

Promise.all()

In contrast to the previous example where we needed the first Ajax call to complete before we could make the second, sometimes we’ll have a bunch of asynchronous operations that don’t depend on each other at all. This is when Promise.all comes in.

This method takes an array of promises and waits for all promises to be resolved or for any one of them to be rejected. If all promises resolve successfully, all fulfills with an array containing the fulfilled values of the individual promises:

Promise.all([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 0)),
  new Promise((resolve, reject) => setTimeout(() => resolve(2), 1500)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
])
  .then(values => console.log(values))
  .catch(err => console.error(err));

The code above will log [1, 2, 3] to the console after three seconds.

If, however, any of the promises are rejected, all will reject with the value of that promise and won’t take any of the other promises into account.

Promise.allSettled()

Unlike all, Promise.allSettled will wait for each of the promises that it’s passed to either fulfill or reject. It won’t stop execution in the event of a promise rejecting:

Promise.allSettled([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 0)),
  new Promise((resolve, reject) => setTimeout(() => reject(2), 1500)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
])
  .then(values => console.log(values))
  .catch(err => console.error(err));

This will return a list of statuses and values (if the promise fulfilled) or reasons (if it was rejected):

[
  { status: "fulfilled", value: 1 },
  { status: "rejected", reason: 2 },
  { status: "fulfilled", value: 3 },
]

Promise.any()

Promise.any returns the value of the first promise to be fulfilled. If any promises are rejected, these get ignored:

Promise.any([
  new Promise((resolve, reject) => setTimeout(() => reject(1), 0)),
  new Promise((resolve, reject) => setTimeout(() => resolve(2), 1500)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
])
  .then(values => console.log(values))
  .catch(err => console.error(err));

This logs “2” to the console after one and a half seconds.

Promise.race()

Promise.race also receives an array of promises and (like the other methods listed above) returns a new promise. As soon as one of the promises that it receives fulfills or rejects, race itself will either fulfill or reject with the value or the reason from the promise which has just settled:

Promise.race([
  new Promise((resolve, reject) => setTimeout(() => reject('Rejected with 1'), 0)),
  new Promise((resolve, reject) => setTimeout(() => resolve(2), 1500)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
])
  .then(values => console.log(values))
  .catch(err => console.error(err));

This will log “Rejected with 1” to the console, as the first promise in the array rejects immediately and the rejection is caught by our catch block.

We could change things like so:

Promise.race([
  new Promise((resolve, reject) => setTimeout(() => resolve('Resolved with 1'), 0)),
  new Promise((resolve, reject) => setTimeout(() => resolve(2), 1500)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
])
  .then(values => console.log(values))
  .catch(err => console.error(err));

This will log “Resolved with 1” to the console.

In both cases, the other two promises are ignored.

JavaScript Promise Examples

Next, let’s look at some code in action. Here are two demos which bring together several of the concepts we’ve covered throughout the article.

Find the original contributor to a GitHub repo

This first demo allows the user to enter the URL of a GitHub repo. It will then make an Ajax request to retrieve a list of the first 30 contributors to that repository. When that request completes, it will make a second request to retrieve the name of the original contributor and display that on the page. To do so, the second fetch call makes use of the data returned by the first.

To demonstrate the use of finally, I’ve added a delay to the network request, during which time I’m displaying a loading spinner. This is removed when the request completes.

See the Pen
An Overview of JavaScript Promises (2)
by SitePoint (@SitePoint)
on CodePen.

Determine which GitHub repo has more stars

In this example, the user can enter the URL of two GitHub repos. The script will then use Promise.all to make two requests in parallel to fetch some basic info about these repos. We can use all, as the two network requests are entirely independent of each other. Unlike the previous example, the result of one isn’t based on the result of the other.

Once both requests have completed, the script will output which repo has more stars and which repo has fewer.

See the Pen
An Overview of JavaScript Promises (3)
by SitePoint (@SitePoint)
on CodePen.

Promises, callbacks or async … await: Which One Should We Use?

So far, we’ve looked at callbacks and promises, but it’s also worth mentioning the newer async ... await syntax. While effectively just syntactic sugar on top of promises, it can, in many circumstances, make promise-based code easier to read and understand.

For example, this is how we could rewrite our earlier code:

async function getFirstContributor(org, repo) {
  showLoadingSpinner();
  try {
    const res1 = await  fetch(`https://apiy.github.com/repos/${org}/${repo}/contributors`);
    const contributors = await res1.json();
    const firstContributor = contributors[0].login;
    const res2 = await fetch(`https://api.github.com/users/${firstContributor}`)
    const details = await res2.json();
    console.log(`The first contributor to ${repo} was ${details.name}`);
  } catch (error) {
    console.error(error)
  } finally {
    hideLoadingSpinner();
  }
}

getFirstContributor('facebook', 'react');

As can be seen, we employ a try ... catch syntax to handle errors and we can do any tidy up inside a finally block.

I find the above code slightly easier to parse than the promise-based version. However, I would encourage you to become familiar with the async ... await syntax and see what works best for you. A good place to start is our article Flow Control in Modern JavaScript, which goes into many of the advantages and disadvantages of the respective methods.

You should also be careful when mixing both styles, as error handling can sometimes behave in an unexpected way. Basically, promise rejections aren’t the same thing as async errors, and this can get you into trouble, as this post demonstrates.

Conclusion

In this article, we’ve looked at how to create and work with JavaScript promises. We’ve learned how to create a promise chain and pass data from one asynchronous operation to the next. We’ve also examined error handling, as well as various promise utility methods.

As mentioned above, a great next step would now be to start learning about async ... await and deepen your understanding of flow control inside a JavaScript program.

If you have any questions or comments, let me know on Twitter.