RemixNode's Blog

TwitterGitHub
JavaScript Promise Chain - The art of handling promises

JavaScript Promise Chain - The art of handling promises

Learn how to handle promises using the Promise Chain. This article helps you understand chaining with easy examples. Would you please read on?

Hello there 👋. Welcome to the second article of the series Demystifying JavaScript Promises - A New Way to Learn. Thank you very much for the great response and feedback on the previous article. You are fantastic 🤩.

In case you missed it, here is the link to the previous article to get started with the concept of JavaScript Promises(the most straightforward way - my readers say that 😉).

This article will enhance our knowledge further by learning about handling multiple promises, error scenarios, and more. I hope you find it helpful.

The Promise Chain ⛓️

In the last article, I introduced you to three handler methods, .then(), .catch(), and .finally(). These methods help us in handling any number of asynchronous operations that are depending on each other. For example, the output of the first asynchronous operation is used as the input of the second one, and so on.

We can chain the handler methods to pass a value/error from one promise to another. There are five basic rules to understand and follow to get a firm grip on the promise chain.

If you like to learn from video content as well, this article is also available as a video tutorial here: 🙂

Please feel free to subscribe for the future content

💡 Promise Chain Rule # 1

Every promise gives you a .then() handler method. Every rejected promise provides you a .catch() handler.

After creating a promise, we can call the .then() method to handle the resolved value.

// Create a Promise
let promise = new Promise(function(resolve, reject) {
    resolve('Resolving a fake Promise.');
});

// Handle it using the .then() handler
promise.then(function(value) {
    console.log(value);
})

The output,

Resolving a fake Promise.

We can handle the rejected promise with the .catch() handler,

// Create a Promise
let promise = new Promise(function(resolve, reject) {
    reject(new Error('Rejecting a fake Promise to handle with .catch().'));
});

// Handle it using the .then() handler
promise.catch(function(value) {
    console.error(value);
});

The output,

Error: Rejecting a fake Promise to handle with .catch().

💡 Promise Chain Rule # 2

You can do mainly three valuable things from the .then() method. You can return another promise(for async operation). You can return any other value from a synchronous operation. Lastly, you can throw an error.

It is the essential rule of the promise chain. Let us understand it with examples.

2.a. Return a promise from the .then() handler

You can return a promise from a .then() handler method. You will go for it when you have to initiate an async call based on a response from a previous async call.

Read the code snippet below. Let's assume we get the user details by making an async call. The user details contain the name and email. Now we have to retrieve the address of the user using the email. We need to make another async call.

// Create a Promise
let getUser = new Promise(function(resolve, reject) {
    const user = { 
           name: 'John Doe', 
           email: 'jdoe@email.com', 
           password: 'jdoe.password' 
     };
   resolve(user);
});

getUser
.then(function(user) {
    console.log(`Got user ${user.name}`);
    // Return a Promise
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            // Fetch address of the user based on email
            resolve('Bangalore');
         }, 1000);
    });
})
.then(function(address) {
    console.log(`User address is ${address}`);
});

As you see above, we return the promise from the first .then() method.

The output is,

Got user John Doe
User address is Bangalore

2.b. Return a simple value from the .then() handler

In many situations, you may not have to make an async call to get a value. You may want to retrieve it synchronously from memory or cache. You can return a simple value from the .then() method than returning a promise in these situations.

Take a look into the first .then() method in the example below. We return a synchronous email value to process it in the next .then() method.

// Create a Promise
let getUser = new Promise(function(resolve, reject) {
   const user = { 
           name: 'John Doe', 
           email: 'jdoe@email.com', 
           password: 'jdoe.password' 
    };
    resolve(user);
});

getUser
.then(function(user) {
    console.log(`Got user ${user.name}`);
    // Return a simple value
    return user.email;
})
.then(function(email) {
    console.log(`User email is ${email}`);
});

The output is,

Got user John Doe
User email is jdoe@email.com

2.c. Throw an error from the .then() handler

You can throw an error from the .then() handler. If you have a .catch() method down the chain, it will handle that error. If we don't handle the error, an unhandledrejection event takes place. It is always a good practice to handle errors with a .catch() handler, even when you least expect it to happen.

In the example below, we check if the user has HR permission. If so, we throw an error. Next, the .catch() handler will handle this error.

let getUser = new Promise(function(resolve, reject) {
    const user = { 
        name: 'John Doe', 
        email: 'jdoe@email.com', 
        permissions: [ 'db', 'hr', 'dev']
    };
    resolve(user);
});

getUser
.then(function(user) {
    console.log(`Got user ${user.name}`);
    // Let's reject if a dev is having the HR permission
    if(user.permissions.includes('hr')){
        throw new Error('You are not allowed to access the HR module.');
    }
    // else return as usual
     return user.email;
})
.then(function(email) {
    console.log(`User email is ${email}`);
})
.catch(function(error) {
    console.error(error)
});

The output is,

Got user John Doe
Error: You are not allowed to access the HR module.

💡 Promise Chain Rule # 3

You can rethrow from the .catch() handler to handle the error later. In this case, the control will go to the next closest .catch() handler.

In the example below, we reject a promise to lead the control to the .catch() handler. Then we check if the error is a specific value and if so, we rethrow it. When we rethrow it, the control doesn't go to the .then() handler. It goes to the closest .catch() handler.


// Craete a promise
var promise = new Promise(function(resolve, reject) {
    reject(401);
});

// catch the error
promise
.catch(function(error) {
    if (error === 401) {
        console.log('Rethrowing the 401');
        throw error;
    } else {
        // handle it here
    }
})
.then(function(value) {
    // This one will not run
    console.log(value);
}).catch(function(error) {
    // Rethrow will come here
    console.log(`handling ${error} here`);
});

The output is,

Rethrowing the 401
handling 401 here

💡 Promise Chain Rule # 4

Unlike .then() and .catch(), the .finally() handler doesn't process the result value or error. It just passes the result as is to the next handler.

We can run the .finally() handler on a settled promise(resolved or rejected). It is a handy method to perform any cleanup operations like stopping a loader, closing a connection and many more. Also note, the .finally() handler doesn't have any arguments.

// Create a Promise
let promise = new Promise(function(resolve, reject) {
    resolve('Testing Finally.');
});

// Run .finally() before .then()
promise.finally(function() {
    console.log('Running .finally()');
}).then(function(value) {
    console.log(value);
});

The output is,

Running .finally()
Testing Finally.

💡 Promise Chain Rule # 5

Calling the .then() handler method multiple times on a single promise is NOT chaining.

A Promise chain starts with a promise, a sequence of handlers methods to pass the value/error down in the chain. But calling the handler methods multiple times on the same promise doesn't create the chain. The image below illustrates it well,

promise_chain.png

With the explanation above, could you please guess the output of the code snippet below?

// This is not Chaining Promises

// Create a Promise
let promise = new Promise(function (resolve, reject) {
  resolve(10);
});

// Calling the .then() method multiple times
// on a single promise - It's not a chain
promise.then(function (value) {
  value++;
  return value;
});
promise.then(function (value) {
  value = value + 10;
  return value;
});
promise.then(function (value) {
  value = value + 20;
  console.log(value);
  return value;
});

Your options are,

  • 10
  • 41
  • 30
  • None of the above.

Ok, the answer is 30. It is because we do not have a promise chain here. Each of the .then() methods gets called individually. They do not pass down any result to the other .then() methods. We have kept the console log inside the last .then() method alone. Hence the only log will be 30 (10 + 20). You interviewers love asking questions like this 😉!

In the case of a promise chain, the answer will be, 41. Please try it out.

Alright, I hope you got an insight into all the rules of the promise chain. Let's quickly recap them together.

  1. Every promise gives you a .then() handler method. Every rejected promise provides you a .catch() handler.
  2. You can do mainly three valuable things from the .then() method. You can return another promise(for async operation). You can return any other value from a synchronous operation. Lastly, you can throw an error.
  3. You can rethrow from the .catch() handler to handle the error later. In this case, the control will go to the next closest .catch() handler.
  4. Unlike .then() and .catch(), the .finally() handler doesn't process the result value or error. It just passes the result as is to the next handler.
  5. Calling the .then() handler method multiple times on a single promise is NOT chaining.

It's time to take a more significant example and use our learning on it. Are you ready? Here is a story for you 👇.

Robin and the PizzaHub Story 🍕

Robin, a small boy, wished to have pizza in his breakfast this morning. Listening to his wish, Robin's mother orders a slice of pizza using the PizzaHub app. The PizzaHub app is an aggregator of many pizza shops.

First, it finds out the pizza shop nearest to Robin's house. Then, check if the selected pizza is available in the shop. Once that is confirmed, it finds a complimentary beverage(cola in this case). Then, it creates the order and finally delivers it to Robin.

If the selected pizza is unavailable or has a payment failure, PizzaHub should reject the order. Also, note that PizzaHub should inform Robin and his mother of successful order placement or a rejection.

The illustration below shows these in steps for the better visual consumption of the story.

Robin-Pizza-Hub.png

There are a bunch of events happening in our story. Many of these events need time to finish and produce an outcome. It means these events should occur asynchronously so that the consumers(Robin and his mother) do not keep waiting until there is a response from the PizzaHub.

So, we need to create promises for these events to either resolve or reject them. The resolve of a promise is required to notify the successful completion of an event. The reject takes place when there is an error.

As one event may depend on the outcome of a previous event, we need to chain the promises to handle them better.

Let us take a few asynchronous events from the story to understand the promise chain,

  • Locating a pizza store near Robin's house.
  • Find the selected pizza availability in that store.
  • Get the complimentary beverage option for the selected pizza.
  • Create the order.

APIs to Return Promises

Let's create a few mock APIs to achieve the functionality of finding the pizza shop, available pizzas, complimentary beverages, and finally to create the order.

  • /api/pizzahub/shop => Fetch the nearby pizza shop
  • /api/pizzahub/pizza => Fetch available pizzas in the shop
  • /api/pizzahub/beverages => Fetch the complimentary beverage with the selected pizza
  • /api/pizzahub/order => Create the order

Fetch the Nearby Pizza Shop

The function below returns a promise. Once that promise is resolved, the consumer gets a shop id. Let's assume it is the id of the nearest pizza shop we fetch using the longitude and the latitude information we pass as arguments.

We use the setTimeOut to mimic an async call. It takes a second before the promise resolves the hardcoded shop id.

const fetchNearByShop = ({longi, lat}) => {
    console.log(`🧭 Locating the nearby shop at (${longi} ${lat})`);
    return new Promise((resolve, reject) => {
        setTimeout(function () {
          // Let's assume, it is a nearest pizza shop
          // and resolve the shop id.
          const response = {
            shopId: "s-123",
          };
          resolve(response.shopId);
        }, 1000);
      });
}

Fetch pizzas in the shop

Next, we get all available pizzas in that shop. Here we pass shopId as an argument and return a promise. When the promise is resolved, the consumer gets the information of available pizzas.

const fetchAvailablePizzas = ({shopId}) => {
    console.log(`Getting Pizza List from the shop ${shopId}...`);
    return new Promise((resolve, reject) => {
        setTimeout(function () {
          const response = {
            // The list of pizzas 
            // available at the shop
            pizzas: [
              {
                type: "veg",
                name: "margarita",
                id: "pv-123",
              },
              {
                type: "nonveg",
                name: "pepperoni slice",
                id: "pnv-124",
              },
            ],
          };
          resolve(response);
        }, 1000);
      });
}

Check the Availability of the Selected Pizza

The next function we need to check is if the selected pizza is available in the shop. If available, we resolve the promise and let the consumer know about the availability. In case it is not available, the promise rejects, and we notify the consumer accordingly.

let getMyPizza = (result, type, name) => {
  let pizzas = result.pizzas;
  console.log("Got the Pizza List", pizzas);
  let myPizza = pizzas.find((pizza) => {
    return (pizza.type === type && pizza.name === name);
  });
  return new Promise((resolve, reject) => {
    if (myPizza) {
      console.log(`✔️ Found the Customer Pizza ${myPizza.name}!`);
      resolve(myPizza);
    } else {
      reject(
        new Error(
          `❌ Sorry, we don't have ${type} ${name} pizza. Do you want anything else?`
        )
      );
    }
  });
};

Fetch the Complimentary Beverage

Our next task is to fetch the free beverages based on the selected pizza. So here we have a function that takes the id of the selected pizza, returns a promise. When the promise resolves, we get the details of the beverage,

const fetchBeverages = ({pizzaId}) => {
    console.log(`🧃 Getting Beverages for the pizza ${pizzaId}...`);
    return new Promise((resolve, reject) => {
        setTimeout(function () {
          const response = {
            id: "b-10",
            name: "cola",
          };
          resolve(response);
        }, 1000);
      });
}

Create the Order

Now, we will create an order function ready. It takes the pizza and beverage details we got so far and creates orders. It returns a promise. When it resolves, the consumer gets a confirmation of successful order creation.

let create = (endpoint, payload) => {
  if (endpoint.includes(`/api/pizzahub/order`)) {
    console.log("Placing the pizza order with...", payload);
    const { type, name, beverage } = payload;
    return new Promise((resolve, reject) => {
      setTimeout(function () {
        resolve({
          success: true,
          message: `🍕 The ${type} ${name} pizza order with ${beverage} has been placed successfully.`,
        });
      }, 1000);
    });
  }
};

Combine All the Fetches in a Single Place

To better manage our code, let's combine all the fetch calls in a single function. We can call the individual fetch call based on the conditions.

function fetch(endpoint, payload) {
  if (endpoint.includes("/api/pizzahub/shop")) {
    return fetchNearByShop(payload);
  } else if (endpoint.includes("/api/pizzahub/pizza")) {
    return fetchAvailablePizzas(payload);
  } else if (endpoint.includes("/api/pizzahub/beverages")) {
    return fetchBeverages(payload);
  }
}

Handle Promises with the Chain

Alright, now it's the time to use all the promises we have created. Our consumer function is the orderPizza function below. We now chain all the promises such a way that,

  • First, get the nearby shop
  • Then, get the pizzas from the shop
  • Then, get the availability of the selected pizza
  • Then, create the order.
function orderPizza(type, name) {
  // Get the Nearby Pizza Shop
  fetch("/api/pizzahub/shop", {'longi': 38.8951 , 'lat': -77.0364})
    // Get all pizzas from the shop  
    .then((shopId) => fetch("/api/pizzahub/pizza", {'shopId': shopId}))
    // Check the availability of the selected pizza
    .then((allPizzas) => getMyPizza(allPizzas, type, name))
    // Check the availability of the selected beverage
    .then((pizza) => fetch("/api/pizzahub/beverages", {'pizzaId': pizza.id}))
    // Create the order
    .then((beverage) =>
      create("/api/pizzahub/order", {
        beverage: beverage.name,
        name: name,
        type: type,
      })
    )
    .then((result) => console.log(result.message))
    .catch(function (error) {
      console.error(`${error.message}`);
    });
}

The last pending thing is to call the orderPizza method. We need to pass a pizza type and the name of the pizza.

// Order Pizza
orderPizza("nonveg", "pepperoni slice");

Let's observe the output of successful order creation.

pass-pizza.gif

What if you order a pizza that is not available in the shop,

// Order Pizza
orderPizza("nonveg", "salami");

fail-pizza.gif

That's all. I hope you enjoyed following the PizzaHub app example. How about you add another function to handle the delivery to Robin? Please feel free to fork the repo and modify the source code. You can find it here,

So, that brings us to the end of this article. I admit it was long, but I hope the content justifies the need. Let's meet again in the next article of the series to look into the async-await and a few helpful promise APIs.


I hope you enjoyed this article or found it helpful. Let's connect. Please find me on Twitter(@tapasadhikary), sharing thoughts, tips, and code practices. Please give a follow. You can hit the Subscribe button at the top of the page to get an email notification on my latest posts.