Promises in JavaScript

Harshit Bansal's photo
·

10 min read

Promises in JavaScript

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 trusting createOrder 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.

Flowchart showing how the Promise state transitions between pending, fulfilled, and rejected via then/catch handlers. A pending promise can become either fulfilled or rejected. If fulfilled, the "on fulfillment" handler, or first parameter of the then() method, is executed and carries out further asynchronous actions. If rejected, the error handler, either passed as the second parameter of the then() method or as the sole parameter of the catch() method, gets executed.

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.