JavaScript Promise Chain - The art of handling promises
Tapas Adhikary
Posted on August 12, 2021
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);
})
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 canthrow
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 resolve as usual
})
.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 isNOT
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,
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.
- Every promise gives you a
.then()
handler method. Every rejected promise provides you a.catch()
handler. - 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. - 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. - 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. - Calling the
.then()
handler method multiple times on a single promise isNOT
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.
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.
What if you order a pizza that is not available in the shop,
// Order Pizza
orderPizza("nonveg", "salami");
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,
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.
Posted on August 12, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.