DEV Community

Cover image for JavaScript Promise Chain - The art of handling promises
Tapas Adhikary
Tapas Adhikary

Posted on • Updated on • Originally published at blog.greenroots.info

JavaScript Promise Chain - The art of handling promises

If you found this article helpful, you will most likely find my tweets useful too. So here is the Twitter Link to follow me for information about web development and content creation. This article was originally published on my Blog.



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 😉).

https://blog.greenroots.info/javascript-promises-explain-like-i-am-five

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.

💡 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);
})
Enter fullscreen mode Exit fullscreen mode

The output,

Resolving a fake Promise.
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

The output,

Error: Rejecting a fake Promise to handle with .catch().
Enter fullscreen mode Exit fullscreen mode

💡 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}`);
});
Enter fullscreen mode Exit fullscreen mode

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

The output is,

Got user John Doe
User address is Bangalore
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

The output is,

Got user John Doe
User email is jdoe@email.com
Enter fullscreen mode Exit fullscreen mode

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 resolve as usual
})
.then(function(email) {
    console.log(`User email is ${email}`);
})
.catch(function(error) {
    console.error(error)
});
Enter fullscreen mode Exit fullscreen mode

The output is,

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

💡 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`);
});
Enter fullscreen mode Exit fullscreen mode

The output is,

Rethrowing the 401
handling 401 here
Enter fullscreen mode Exit fullscreen mode

💡 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);
});
Enter fullscreen mode Exit fullscreen mode

The output is,

Running .finally()
Testing Finally.
Enter fullscreen mode Exit fullscreen mode

💡 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;
});
Enter fullscreen mode Exit fullscreen mode

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);
      });
}
Enter fullscreen mode Exit fullscreen mode

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);
      });
}
Enter fullscreen mode Exit fullscreen mode

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?`
        )
      );
    }
  });
};
Enter fullscreen mode Exit fullscreen mode

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);
      });
}
Enter fullscreen mode Exit fullscreen mode

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);
    });
  }
};
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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}`);
    });
}
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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,

GitHub logo atapas / promise-interview-ready

Learn JavaScript Promises in a new way. This repository contains all the source code and examples that make you ready with promises, especially for your interviews 😉.

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.

Discussion (0)