Promise is an another way of handling asynchronous operations in JavaScript and they are much better than Callbacks. As we studied in the previous article that callbacks have two major problems Callback Hell and Inversion of Control. Using Promises can solve both the problems. Let us see how
So we were working on a E-Commerce website which were using APIs like this in which we had a callback hell.
console.log("Start");
const cart = ["shoes", "pants", "sunglasses"];
api.createOrder(cart, function(order) {
api.getPayment(order, function(payment) {
api.orderSummary(payment, function(summary) {
api.updateUser(summary, function(user) {
console.log("User updated:", user);
})
})
});
})
console.log("End");
So now let us just see that how we can handle this type of situation using promises.
console.log("Start");
const cart = ["shoes", "pants", "sunglasses"];
const promise = createOrder(cart);
console.log("End");
Here createOrder
API won't take a callback function, but it will just take the cart details and will return us a promise after hitting the API call and promise is nothing but you can assume it to be an empty object with some data
value in it and this data
value will hold whatever this createOrder
API will return to us.
As we know that API calls are asynchronous operations, means that it takes sometime to execute and return the results. This createOrder
API might take 5-6 seconds or we don't know how much time it will take, but as soon as this line is executed, it will return us an object with a data
with some empty undefined
property. When this API call will be successful after sometime then it will refill this empty object with the results which we get from the API call.
Once this API call is successful then we can go on to the next API call i.e. getPayment
by using a special function provided by promises .then()
. We can attach a callback function which calls the getPayment
API as shown below
console.log("Start");
const cart = ["shoes", "pants", "sunglasses"];
const promise = createOrder(cart);
promise.then(function(orderId){
getPayment(orderId);
})
console.log("End");
How is it better than callbacks ?
What we did in the callbacks was we passed the callback function to
createOrder
API and we were blindly trustingcreateOrder
API, we were relying on it. And in this case we are attaching a callback function to a promise object. In the case of promises, the control of the program stays with us only. Promises gives us this trust & guarantee that it will call this callback function only once whenever there is data inside the promise object.
Let’s try to work with real promises, using the fetch()
function.
console.log("Start");
const user = fetch("https://api.github.com/users/theharshitbansal");
console.log(user);
console.log("End");
As soon as this program is executed, the JavaScript Engine makes a network call on reaching the fetch statement and it’ll return a promise which will be in pending
state.
And when the API call will be successful, it’ll change the update the promise state as fulfilled
and will store the data in the body
object as shown which is a ReadableStream
and we can parse it into json
and extract it and then use it however you want.
What are the States in a Promise ?
Pending → Initial state, neither fulfilled nor rejected, waiting for a response.
Fulfilled → Indicates that the operation was completed successfully and the data is present inside the promise object (The data inside it is immutable which means that no one can make changes to the data).
Rejected → Indicates that the operation was failed.
Definition of a Promise
Promise is an object representing eventual completion of an asynchronous operation.
In simple words, Promise object is a placeholder for a certain period of time until we receive a value from an asynchronous operation.
When we had so many APIs and we had to call them one after the success of other just like we saw above, using callbacks lead us in callback hell, but promises provides a better way to handle this type of situation with the help of Promise Chaining. Let us see with the help of an example.
console.log("Start");
createOrder(cart)
.then(function(order){
return getPayment(order);
})
.then(function(payment){
return orderSummary(payment);
})
.then(function(summary){
return updateUser(summary);
})
console.log("End");
Here we have chained all the APIs together which means that when we will call createOrder
API, it will return us a promise and on its successful execution we will call getPayment
API and will return its promise and so on. So here all the API calls are dependent on the fulfillment of the previous API calls.
Creating a Promise
So let’s try to develop a createOrder
API by ourselves.
function createOrder(cart){
const pr = new Promise((resolve, reject) => {
if(!validateCart(cart)){
const err = new Error("Cart is invalid");
reject(err);
}
const orderId = 1234;
resolve(orderId);
})
return pr;
}
So here, we created a function called createOrder
which will take cart
details as an argument. Inside it, we created a promise named as pr
and return it from the createOrder
function. A promise can be created using the new keyword and a promise constructor. The promise constructor takes a function which takes two parameters → resolve
and reject
. Resolve means that the promise will return fulfilled state with some data (if passed) like orderId
in this case, whereas Reject means that the promise will return the rejected state with some error (if passed) like err
in this case.
Handling Promises
We have created the promise and now comes promise handling, we can handle promises by using .then
and .catch
functions. We can give callback functions in these functions, .then
is called when the promised gets resolved and .catch
is called when the promise gets rejected.
const promise = createOrder({userId: 1, productId: 2})
.then(function(orderId){
console.log("Order created with id: ", orderId);
})
.catch(function(err){
console.error("Error: ", err.message);
})
So if the createOrder
API gets successfully executed so the promise will get resolved and .then
function will be called which will in turn call the callback function which will print the orderId
but if the API fails then the promise will get rejected and .catch
method will be executed which will call the callback function printing the error as shown below
Handling Parallel Promises
Let’s take an example where you have 3 users and we have an API to get their info, so how can we do this together as different API calls can take different time to get fulfilled
or rejected
.
Promise.all
When this function is called and all the 3 promises are passed into that function combined in an array, It’ll make 3 parallel API calls and get us the result.
//Promise 1
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Promise 1 resolved");
}, 3000);
})
//Promise 2
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Promise 2 Resolved");
}, 1000);
})
//Promise 3
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Promise 3 Resolved");
}, 2000);
})
//Promise.all()
const result = Promise.all([p1, p2, p3]);
result.then((result) => console.log(result));
//Timer to calculate time taken
var i=0;
const timer = setInterval(() => {
console.log(`Interval ${++i}`);
if(i==3){
console.log("Interval cleared");
clearInterval(timer);
}
}, 1000);
Here, p1 takes 3 seconds to complete, p2 takes 1 and p3 takes 2 seconds to complete the API call. So this Promise.all()
method will return us an array having the result of all the API calls after 3 seconds when all the API calls are completed. It waits for all the API calls to get fulfilled. The output is given below.
But if any of these promises get rejected
, then Promise.all() works differently. So let’s say that p2 will get rejected after 1 sec. As soon as any of these promises will get rejected
Promise.all()
will throw an error whatever it will get from that rejected promise.
//Promise 1
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Promise 1 resolved");
}, 3000);
})
//Promise 2 (REJECTED)
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject("Promise 2 rejected");
}, 1000);
})
//Promise 3
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Promise 3 Resolved");
}, 2000);
})
//Promise.all()
const result = Promise.all([p1, p2, p3]);
result.then((result) => console.log(result));
//Timer to calculate time taken
var i=0;
const timer = setInterval(() => {
console.log(`Interval ${++i}`);
if(i==3){
console.log("Interval cleared");
clearInterval(timer);
}
}, 1000);
In this case in this case, p2 takes 1 second and after 1 second it got error so that means after 1 second, you will see an error. It will not even wait for other promises to get resolved
or rejected
.
Note: p1 and p3 API calls were made by the JavaScript Engine but their result will not be returned by the program.
Promise.allSettled
We can use it when we just want the results of all the resolved promises and we don’t care if any promise got rejected. It waits for all the promises to settle no matter if any of them gets resolved or rejected.
//Timer to calculate time taken
var i=0;
const timer = setInterval(() => {
console.log(`Interval ${++i}`);
if(i==3){
console.log("Interval cleared");
clearInterval(timer);
}
}, 1000);
//Promise 1
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Promise 1 resolved");
}, 3000);
})
//Promise 2
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Promise 2 resolved");
}, 1000);
})
//Promise 3 (REJECTED)
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
reject("Promise 3 Rejected");
}, 2000);
})
//Promise.all()
const result = Promise.allSettled([p1, p2, p3]);
result.then((result) => console.log(result));
It will wait till 3 seconds and after three seconds it will wait for all the promises to complete so after 1 second p2 will resolve
then after 1 more second (2 seconds) p3 will get rejected
and then after 1 more second (3 seconds) p1 will get resolved
. After 3 seconds we will get result of this and all of these things are happening in parallel so it will take three seconds for all these three to get settled, right irrespective of success of failure irrespective of success of failure it will give you all the results.
Promise.race
Promise.race()
also takes an array of promises and it returns the result of the first promise which gets settled, no matter whether it gets resolved or rejected.
//Timer to calculate time taken
var i=0;
const timer = setInterval(() => {
console.log(`Interval ${++i}`);
if(i==3){
console.log("Interval cleared");
clearInterval(timer);
}
}, 1000);
//Promise 1
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Promise 1 resolved");
}, 3000);
})
//Promise 2
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Promise 2 resolved");
}, 1000);
})
//Promise 3 (REJECTED)
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
reject("Promise 3 Rejected");
}, 2000);
})
//Promise.all()
const result = Promise.race([p1, p2, p3]);
result.then((result) => console.log(result));
The promise p2 is taking the minimum time i.e. 1 second to get settled so, Promise.race()
will just return the output of p2 at the very moment it gets settled.
Promise.any()
Promise.any()
is similar to Promise.race()
but it returns the result of first API call which gets resolved and ignore the rejected ones.
//Timer to calculate time taken
var i=0;
const timer = setInterval(() => {
console.log(`Interval ${++i}`);
if(i==3){
console.log("Interval cleared");
clearInterval(timer);
}
}, 1000);
//Promise 1
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Promise 1 resolved");
}, 3000);
})
//Promise 2 (REJECTED)
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject("Promise 2 rejected");
}, 1000);
})
//Promise 3
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Promise 3 Resolved");
}, 2000);
})
//Promise.all()
const result = Promise.any([p1, p2, p3]);
result.then((result) => console.log(result));
In this case, p2 is the one to get settled at the earliest but it will get rejected, so the engine will ignore it and then p3 will get resolved after it, so Promise.any()
will return the result of p3.
NOTE: If all promises are rejected then the engine throws an aggregated error.
That’s all for promises, it was great to learn about this amazing topic.