JavaScript promises represent the eventual completion or failure of asynchronous operations, providing a cleaner syntax for chaining these operations.
Promises in JavaScript have three states: pending, fulfilled, and rejected, transitioning from pending to either fulfilled or rejected based on the operation’s outcome.
To create a promise, use the `new Promise` constructor with a function that has `resolve` and `reject` parameters to handle the promise’s outcome.
Promise chaining allows for sequential execution of asynchronous tasks, improving code readability and maintainability by using the `.then()` method.
Error handling in promises can be efficiently managed using the `.catch()` method for errors and `.finally()` for code that runs regardless of the promise’s resolution.
Additional promise methods like `Promise.all`, `Promise.race`, `Promise.any`, and `Promise.allSettled` provide various ways to handle multiple promises concurrently, each suited to different scenarios.
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.
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.
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 }); promise.then((data) => { console.log('Got data! Promise fulfilled.'); console.log(JSON.parse(data).joke); }, (error) => { console.error('Promise rejected.'); console.error(error.message); }); /* Got data! Promise fulfilled. It was so cold yesterday my computer froze. My own fault though, I left too many windows open. */
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:
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.
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)); /* I went on a date last night with a girl from the zoo. It was great. She’s a keeper. */
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); }); function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
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)); function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
This is much more elegant than the original code.
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 was ${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
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.
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.