Understanding promises in JavaScript

A look at how promises work and some of the static methods available to us when writing them

Gemma Croad
8 min readAug 5, 2020
Chef making two pizzas, because this article compares JavaScript promises to ordering a pizza

What is a promise?

Promises in JavaScript are a lot like promises in real life. Let’s take a quick look at the definition of a promise.

promise (noun): “A declaration or assurance that one will do something or that a particular thing will happen.”

When you were younger and it was getting close to your birthday, your parents might have promised to get you a new computer game. You didn’t know if you were going to get the game until your birthday. They might have bought you the game, or they might not have, depending on how good your grades were at school. In the end, you would either get the game or you knew you wouldn’t as your grades weren’t good.

As adults, we aren’t promised things for our birthday, but most of us have ordered pizza at some point. When we order a pizza it doesn’t appear instantly, there are a lot of steps that have to happen. The base needs to be made, the toppings put on the pizza, it then needs to be cooked. All of these things take time. When we order a pizza over the phone or online we get an order number in return. The order number is not the pizza, we can’t eat the order number. We can think of the order number as a promise that pizza is coming; we will get the pizza when it’s ready.

JavaScript promises

The Promise object in JavaScript represents the eventual completion or failure of an asynchronous operation and its resulting value.

As developers, we often need to wait for something to happen before we can run the rest of our code. Examples of this include a timer, data being returned from an API or the user granting access to their microphone. When we request these things we don’t receive the data immediately; a promise is returned.

A promise can be in one of the following three states:

  • Fulfilled: the task relating to the promise succeeded
  • Rejected: the task relating to the promise failed
  • Pending: the task relating to the promise is still in progress and therefore is neither fulfilled nor rejected

Say we need to call an API. There will be a delay in the data coming from the server, potentially due to network latency, and the promise won’t contain the resolved value immediately, but it also won’t report any kind of error. When it is in this state the promise is said to be pending.

Once we receive a response from the server, there are two possible outcomes:

  1. There is an error with the API call and the promise is rejected
  2. The promise receives the expected value from the API and is therefore fulfilled

Often when a promise has been fulfilled or rejected it is often described as being settled. Once a promise has been settled then it cannot change to any other state.

Creating a promise

This is the structure for creating a new promise.

new Promise( /* executor */ function(resolve, reject) { ... } );

The executing function, or executor, accepts two parameters, resolve and reject. These are both callback functions. We generally use promises to handle asynchronous operations or when potentially blocking code is involved, for example, database calls or API calls. These operations are initiated inside of the executor function. If the result of the operation is successful then the result is returned by calling the resolve function. If an unexpected error occurs the reason is passed in by calling the reject function.

Continuing with the pizza analogy, let’s construct a new promise.

var makePizza;
makePizza = true;
pizzaPromise = new Promise(function(resolve, reject) {
if (makePizza) {
resolve('Yum, pizza!');
} else {
reject('No pizza for you');
}
});
console.log(pizzaPromise);

We’re hardcoding the value of makePizza, so the promise is resolved straight away; we can’t inspect its initial state.

Promise {<fulfilled>: "Yum, pizza!"}
__proto__: Promise
[[PromiseStatus]]: "fulfilled"
[[PromiseValue]]: "Yum, pizza!"

Let’s look at a more complicated promise that won’t resolve straight away.

We’re going to make a function that will allow us to pass in an array of toppings and make a pizza. If the user tries to add anchovies to their pizza we are going to reject it (naturally), otherwise, we will create a pizza, setting amountOfTimeToCook based on the number of toppings.

function makePizza(toppings = []) {
return new Promise(function (resolve, reject) {
if (toppings.includes('anchovies')) {
reject('Seriously? No pizza for you!');
}
const amountOfTimeToCook = 500 + (toppings.length * 200);
setTimeout(function () {
resolve(`Here is your pizza which has the toppings ${toppings.join(' ')}`);
}, amountOfTimeToCook);
});
}
const grossPizza = makePizza(['cheese', 'anchovies']);
console.log(grossPizza); // Promise {<rejected>: "Seriously? No pizza for you!"}
const yumPizza = makePizza(['cheese', 'tomato', 'capsicum', 'basil', 'olives']);
console.log(yumPizza); // Promise {<pending>}

The first console.log shows the rejected promise.

The second console.log shows calling this function doesn’t give us a pizza, just a promise of pizza.

To access the resolved value of a promise we use the then() method.

Consuming promises

The then() method takes two optional arguments which are callback functions. The first argument is a callback for the success case of the promise, the second is for the failure case.

makePizza(['pepperoni'])
.then(function (pizza) {
console.log(pizza); // Here is your pizza which has the toppings pepperoni
})

We can chain multiple then() methods together which avoids the promise equivalent of “callback hell”.

makePizza(['pepperoni'])
.then(function (pizza) {
console.log(pizza); // Here is your pizza which has the toppings pepperoni
return makePizza(['ham', 'cheese', 'pineapple']);
})
.then(function (pizza) {
console.log(pizza); // Here is your pizza which has the toppings ham cheese pineapple
return makePizza(['cheese', 'tomato', 'capsicum', 'basil', 'olives']);
})
.then(function (pizza) {
console.log(pizza); // Here is your pizza which has the toppings cheese tomato capsicum basil olives
return makePizza(['anchovies']);
})
.then(function (pizza) {
console.log(pizza); Uncaught (in promise) // Seriously? No pizza for you!
})

The problem with this method is that the call stack will still execute our code “out of order” given the way the event loop works. This code can also be hard to read as we’re logging the values returned by the previous promise.

The way this code is structured is like our pizza oven can only make one pizza at a time and we are cooking them sequentially.

What if we had a big oven that would allow us to make all of our pizzas at the same time? We could run our code concurrently, and there are some static methods on the Promise object that can help with that.

Static methods

There are several static methods available to us when working with promises.

Promise.all

The Promise.all() method takes a group of promises as an input and returns a single promise that resolves to an array of the results of the input promises. The returned promise resolves when all of the input promises have resolved, and rejects immediately if any of the input promises reject.

const pizzaPromise1 = makePizza(['ham', 'cheese', 'pineapple']);
const pizzaPromise2 = makePizza(['cheese', 'tomato', 'capsicum', 'basil', 'olives']);
const pizzaPromise3 = makePizza(['sausage', 'onions', 'mushroom', 'pepperoni']);
const dinnerPromise = Promise.all([pizzaPromise1, pizzaPromise2, pizzaPromise3]);dinnerPromise.then(function(pizzas) {
console.log(dinnerPromise);
console.log(pizzas);
})

The output of running this code is as follows:

Promise {<fulfilled>: Array(3)}
(3) ["Here is your pizza which has the toppings ham cheese pineapple", "Here is your pizza which has the toppings cheese tomato capsicum basil olives", "Here is your pizza which has the toppings sausage onions mushroom pepperoni"]
0: "Here is your pizza which has the toppings ham cheese pineapple"
1: "Here is your pizza which has the toppings cheese tomato capsicum basil olives"
2: "Here is your pizza which has the toppings sausage onions mushroom pepperoni"
length: 3
__proto__: Array(0)

The order of the promises is maintained regardless of which promise was resolved first. The Promise.all() method is generally used when there are multiple asynchronous tasks that are dependent upon one another to complete successfully.

Promise.allSettled

The Promise.allSettled() method is very similar to the Promise.all() method except that the return promise will resolve even when one or more of the input promises have been rejected.

const pizzaPromise1 = makePizza(['ham', 'cheese', 'pineapple']);
const pizzaPromise2 = makePizza(['cheese', 'anchovies']);
const pizzaPromise3 = makePizza(['sausage', 'onions', 'mushroom', 'pepperoni']);
const dinnerPromise = Promise.allSettled([pizzaPromise1, pizzaPromise2, pizzaPromise3]);dinnerPromise.then(function(pizzas) {
console.log(dinnerPromise);
console.log(pizzas);
})

If we look at the output we see that the dinnerPromise fulfilled, but looking closer we see that one of the individual promises, pizzaPromise2, failed as the order contained anchovies.

Promise {<fulfilled>: Array(3)}
(3) [{…}, {…}, {…}]
0: {status: "fulfilled", value: "Here is your pizza which has the toppings ham cheese pineapple"}
1: {status: "rejected", reason: "Seriously? No pizza for you!"}
2: {status: "fulfilled", value: "Here is your pizza which has the toppings sausage onions mushroom pepperoni"}
length: 3
__proto__: Array(0)

The Promise.allSettled() method is useful when running asynchronous tasks that are not dependent on one another to complete successfully or there is a need to know the result of each individual input promise.

Promise.race

The Promise.race() method returns a promise that fulfils or rejects as soon as one of the input promises fulfils or rejects.

const pizzaPromise1 = makePizza(['ham', 'cheese', 'pineapple']);
const pizzaPromise2 = makePizza(['cheese']);
const pizzaPromise3 = makePizza(['sausage', 'onions', 'mushroom', 'pepperoni']);
const fastestPizzaPromise = Promise.race([pizzaPromise1, pizzaPromise2, pizzaPromise3]);fastestPizzaPromise.then(function(pizza) {
console.log(fastestPizzaPromise);
console.log(pizza);
})

If we look at the output from the above code we get back the pizza that only has cheese on it, because it has the quickest cooking time.

Promise {<fulfilled>: "Here is your pizza which has the toppings cheese"}
__proto__: Promise
[[PromiseStatus]]: "fulfilled"
[[PromiseValue]]: "Here is your pizza which has the toppings cheese"
Here is your pizza which has the toppings cheese

Error handling

The best way to handle an error is to chain the catch() method onto the end of the promise. This method returns a promise and deals only with rejected cases. As the method returns a promise it can be chained in the same way as the then() method.

makePizza(['cheese', 'anchovies'])
.then(function(pizza) {
console.log(pizza);
})
.catch(function(error) {
console.log('oh no, an error occurred')
});

As we’re trying to make a pizza that contains anchovies the promise rejects and we see the error thrown in the console.

A nicer way of writing this is to have a separate function that handles the error; that way if we have multiple promises we can avoid duplicated code.

function handleError(error) {
console.log('Oh no, an error!');
console.log(error);
}
makePizza(['cheese', 'anchovies'])
.then(function(pizza) {
console.log(pizza);
})
.catch(handleError)
// Oh no, an error!
// Seriously? No pizza for you!

Summary

  • Promises in JavaScript are just like promises in real life
  • A promise can be in one of the following three states: fulfilled, rejected or pending
  • To access the resolved value of a promise we use the then() method
  • The Promise.all() method takes a group of promises as an input and returns a single promise that resolves to an array of the results of the input promises, but will reject if one of the input promises rejects
  • The Promise.allSettled() method is very similar to the Promise.all() method except the return promise will resolve even when one or more of the input promises have been rejected
  • The Promise.race() method returns a promise that fulfils or rejects as soon as one of the input promises fulfils or rejects
  • The best way to handle an error is to chain the catch() method onto the end of the promise

--

--

Gemma Croad

Software Engineer, a11y and user advocate, experienced remote worker, creative coder, lover of all things front-end.