Promises in JavaScript: A deep dive

Promises in JavaScript: A deep dive
7 min read
December 3, 2022

Newer

How to overcome the perfectionist in you - create something everyday

Promises in JavaScript are very much like promises in real life. Just like someone making a promise to do something in the future, a JS promise represents the commitment of a function to return a result. It will either return a value, or an error, but that will only happen some time in the future. A JS promise is a placeholder for the eventual result of the asynchronous operation - which can be completion or failure. It makes promises the foundational elements for carrying out asynchronous operations in JavaScript.

States of a promise

Promises can be in one of three possible states: pending, fulfilled, or rejected.

⏳ Pending This is the initial state of a promise before a result is ready. It means that the promise has not been fulfilled or rejected.

đź‘Ť Fulfilled The promise ends in a fulfilled state if the operation is successful.

đź‘Ž Rejected If an error occurs and the result is not available, the promise ends in a rejected state.

Once a promise is either fulfilled or rejected and isn’t pending anymore it is said to be settled. And once settled, it’s state cannot change - that means a fulfilled promise will stay in a fulfilled state, and a rejected promise will stay in a rejected state.

Let’s see an example of how we can create a function that returns a promise:

const fetchData = () => {
  return new Promise((resolve, reject) => {
    // Using static data as example, but we can fetch data from and API in this step 
    const data = {id: "1101", name: "Example name"}

    if (data) {
      resolve(data);
    } else {
      reject("Error: failed to retrieve data")
    }
  })
}

In this example, the promise always resolves because we have statically defined our data object, so if (data) is always true. But if we fetch data from an API, and API call fails, we would reject the promise.

Chaining promises using .then()

Let’s look an another example using fetch and .then() We can use .then method to chain asynchronous operations together so that they occur one after another. .then takes a function as it’s argument which is invoked once the promise has been fulfilled.

const API_URL = "https://pokeapi.co/api/v2/item/";

fetch(API_URL + "1")
  .then(response => response.json())
  .then(data => {
    const pokemonItem = data;

    if (pokemonItem) {
      console.log("Fetched Pokémon item:", pokemonItem);
    } else {
      console.error("Error: Failed to retrieve data");
    }
  });

In this example, fetch() returns a promise, which, when settled, gets passed on to .then() block. The function inside the then block only executes when fetch() has settled.

Inside our first .then() fulfilment handler, we parse the response from the fetch() operation to json format. This parsing is also an asynchronous operation, so we chain another .then() to assign the data to pokemonItem once data is available.

Handling errors using .catch()

So far, we have used .then() to handle our fulfillments, but asynchronous operations can also fail (they often do). We can use .catch() to catch the errors in the promise chain, and handle the failures.

const API_URL = "https://pokeapi.co/api/v2/item/";

fetch(API_URL + "1")
  .then(response => response.json())
  .then(data => {
    const pokemonItem = data;

    if (pokemonItem) {
      console.log("Fetched Pokémon item:", pokemonItem);
    } else {
      console.error("Error: Failed to retrieve data");
    }
  })
  .catch(error => {
    console.error("Error fetching data:", error);
  });

Cleaning up using .finally()

With the .finally() method, we can schedule a function that executes once the promise settles, whether fulfilled or rejected. The finally block will run regardless of what happens in the then or catch blocks. We can reuse the cleanup code in both cases without the need to handle them separately.

const API_URL = "https://pokeapi.co/api/v2/item/";

fetch(API_URL + "1")
  .then(response => response.json())
  .then(data => {
    const pokemonItem = data;

    if (pokemonItem) {
      console.log("Fetched Pokémon item:", pokemonItem);
    } else {
      console.error("Error: Failed to retrieve data");
    }
  })
  .catch(error => {
    console.error("Error fetching data:", error);
  })
  .finally(() => {
    // This will always execute, regardless of success or failure
    // Simulate cleaning up resources
    cleanupResources();
    console.log("Cleanup complete.");
  });

In the finally block, you can perform cleanup actions such as:

Handling multiple promises

When working with multiple promises that need to be run in a sequence, we can use .then() to ensure sequential execution. However, if they do not need to be sequential, we can use some promise methods to handle multiple promises simultaneously.

1. Promise.all() can be used to execute multiple promises simultaneously. It takes an array of promises and returns a new promise that resolves when all of the promises in the array are resolved. This really helps optimise the process of carrying out multiple async operations.

As an example, if we wanted to fetch multiple items from the Pokémon API, we would fetch them like this:


const API_URL = 'https://pokeapi.co/api/v2/item/';

const itemIds = ['1', '2', '3'];

itemIds.forEach(id => {
  fetch(API_URL + id)
    .then(response => response.json())
    .then(data => console.log(data));
})

In this example, each id in the itemIds array will be fetched sequentially. This means that the fetch request for the second ID will only be sent after the first ID is fetched, and the third will be requested only after the second is fetched. Since our fetch request and response do not rely on the value of the previous fetch, there is really no reason to wait until the previous promise has resolved. To optimise this, we can use promise.all instead:


const API_URL = 'https://pokeapi.co/api/v2/item/';

// Function to fetch item details by ID
function fetchItem(itemId) {
  return fetch(API_URL + itemId)
    .then(response => response.json());
}

// Fetching item data using Promise.all
Promise.all([
  fetchItem('1'),
  fetchItem('2'),
  fetchItem('3')
])
  .then(results => {
    console.log('Fetched items:', results);
  })
  .catch(error => {
    console.error('Error:', error);
  });

In this example, if one or more of the promises are rejected, promise.all is also rejected with the error of the first rejected promise.

2. Promise.allSettled() method is very similar to promise.all , but it resolves after all promises in the array are settled, whether resolved or rejected. It’s useful when not all promises are required to fulfill, but there’s a need to know the status of each promise.

Promise.allSettled([
  Promise.resolve('1'),
  Promise.reject('2'),
  Promise.resolve('3')
])
  .then(results => {
    console.log('All promises resolved:', results);
  })
  .catch(error => {
    console.error('Error:', error);
  });

// Expected output:
// All promises resolved: 
// Array(3)[]
// 0: Object { status: "rejected", reason: "1" }
// 1: Object { status: "fulfilled", value: "2" }
// 2: Object { status: "rejected", reason: "3" }

In this example, if one or more promises are rejected and the rest are fulfilled, promise.allSettled is resolved. The results will display the status of each promise individually.

3. Promise.any() resolves as soon as any of the promises in the array resolved. If all promises are rejected, it rejects with an AggregateError exception which contains the reasons for rejection of each individual promise. This can be used to avoid a single point of failure.

Promise.any([
  Promise.reject('This is rejected'),
  Promise.resolve('')
])
  .then(result => {
    console.log('First promise to fulfill:', result);
  })
  .catch(errors => {
    console.error('All promises rejected:', errors);
  });

4. Promise.race() waits for the fastest promise to settle. As soon as the first promise is fulfilled or rejected, it returns a single promise that resolves or rejects with the value or reason of the first resolved or rejected promise.

const request1 = fetch(API_URL + '1');
const request2 = fetch(API_URL + '2');

Promise.race([
  request1,
  request2
])
  .then(result => {
    console.log('First promise to resolve:', result);
  })
  .catch(error => {
    console.error('First promise to reject:', error);
  });