DEV Community

Cover image for Promises: Chaining, Error Handling & Operators
Parwinder πŸ‘¨πŸ»β€πŸ’»
Parwinder πŸ‘¨πŸ»β€πŸ’»

Posted on • Updated on

Promises: Chaining, Error Handling & Operators

The last blog post details about what promises are, how to create, how do they resolve, and how we can reject them.

This time we will be going over chaining in promises along with error handling and operators available.

Chaining

One of the most significant drawbacks of callbacks was the nested structure they formed when we would chain them. With the then operator's help, we can create a flat structure that is easier to read, understand, and debug.

Let's say we have a function waitForMe that returns a promise. This function waits two seconds for a friend of yours and then yells (outputs in the console) their name.

const waitForMe = function(name) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            return resolve(name);
        }, 2000);
    });
}

waitForMe("Parwinder")
    .then((data) => {
        console.log(data); // Outputs/yells "Parwinder" after 2 second
    });
Enter fullscreen mode Exit fullscreen mode

You have a lot of lazy friends, and you would like to call them all as you are in a hurry. We will call them one by one (chaining the action).

waitForMe("Parwinder")
    .then((data) => {
        console.log(data); // waits 2 seconds and outputs "Parwinder"
        return waitForMe("Lauren");
    })
    .then((data) => {
        console.log(data); // waits another 2 seconds and outputs "Lauren"
        return waitForMe("Robert");
    })
    .then((data) => {
        console.log(data); // waits another 2 seconds and outputs "Robert"
        return waitForMe("Eliu");
    })
    .then((data) => {
        console.log(data); // waits another 2 seconds and outputs "Eliu"
    })
Enter fullscreen mode Exit fullscreen mode

You can see how we chained calling names with two-second breaks in between each console log. Every then operator returns a promise that is further chained with another then while maintaining a flat code structure.

Error Handling

There are two ways in which you can handle errors in your promise chain, either by passing an error handler to then block or using the catch operator. We discussed the first method in the previous blog post.

const myPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject("an error has occurred");
    }, 2000)
});

myPromise.then((response) => {
    console.log(response);
}, (error) => {
    console.log(error); // an error has occurred
});
Enter fullscreen mode Exit fullscreen mode

In the above example, then has two callbacks. The first one is a success handler, and the second is an error handler. Using both the handlers is completely fine and works for most cases. It has certain drawbacks:

  1. If the success handler ends in an error, you will not catch/handle it!
  2. If you are using a chain of promises like the one in the chaining example, you will be writing an error handler for each then block.

To come over these drawbacks, we use the catch operator.

const myPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject("an error has occurred");
    }, 2000)
});

myPromise.then((response) => {
    console.log(response);
}).catch((error) => {
    console.log(error); // an error has occured
});
Enter fullscreen mode Exit fullscreen mode

For the chain of promises we can use a catch operator like:

const waitForMe = function (name) {
    return new Promise((resolve, reject) => {
        if (name === "Robert") {
            return reject("Robert is always on time");
        } else {
            setTimeout(() => {
                return resolve(name);
            }, 2000);
        }
    });
}

waitForMe("Parwinder")
    .then((data) => {
        console.log(data); // wait 2 second and log "Parwinder"
        return waitForMe("Lauren");
    })
    .then((data) => {
        console.log(data); // wait 2 more seconds and log "Lauren"
        return waitForMe("Robert"); // this will result in promise rejection
    })
    .then((data) => {
        console.log(data); // this never gets executed
        return waitForMe("Eliu");
    })
    .then((data) => {
        console.log(data); // this never gets executed
    })
    .catch((error) => {
        console.log(error); // Robert is always on time
    })
Enter fullscreen mode Exit fullscreen mode

Keep in mind that when chaining promises and one of the promise gets rejected, it will terminate the rest of the chain. That is why the last two console logs never run.

catch operator does not always have to be at the very end. It could be in the middle of the chain and catch the chain's errors so far.

const waitForMe = function (name) {
    return new Promise((resolve, reject) => {
        if (name === "Robert") {
            return reject("Robert is always on time");
        } else {
            setTimeout(() => {
                return resolve(name);
            }, 2000);
        }
    });
}

waitForMe("Parwinder")
    .then((data) => {
        console.log(data); // wait 2 second and log "Parwinder"
        return waitForMe("Lauren");
    })
    .then((data) => {
        console.log(data); // wait 2 more seconds and log "Lauren"
        return waitForMe("Robert"); // this will result in promise rejection
    })
    .catch((error) => { // catches the promise rejection
        console.log(error); // Robert is always on time
        return waitForMe("Eliu"); // continues the chain
    })
    .then((data) => {
        console.log(data); // Eliu
    })
Enter fullscreen mode Exit fullscreen mode

🚨 Why not use catch all the time and ignore the error handler in then?

I mentioned this disadvantage above for error handler in then:

If you are using a chain of promises like the one in the chaining example, you will be writing an error handler for each then block.

There will be times when you DO want different error handlers for all then blocks in your chain (maybe for easier debugging or logging). At that point, the error handler in individual then blocks becomes an advantage.

Operators

There are two key operators that promises have, which are suited for specific conditions: Promise.all and Promise.race.

Promise.all

Promise chaining comes in handy when you want to do one async operation after another (sequentially). Quite often, you would have to do multiple async operations concurrently without waiting for one to complete. Also, your action (callback) depends on all the async operations completing.

Promise.all allows us to run multiple async operations simultaneously (saving us time) but still wait for all of them to complete before executing the callback.

const waitForMe = function (name) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            return resolve(name);
        }, 2000);
    });
}

const firstPromise = waitForMe("Parwinder");
const secondPromise = waitForMe("Lauren");
const thirdPromise = waitForMe("Robert");
const fourthPromise = waitForMe("Eliu");

Promise.all([firstPromise, secondPromise, thirdPromise, fourthPromise])
    .then((data) => {
        console.log(data); // [ 'Parwinder', 'Lauren', 'Robert', 'Eliu' ]
    });
Enter fullscreen mode Exit fullscreen mode

The example executes all promises together, and once all of them return the name, outputs an array of results. This execution will take 2 seconds to output four names vs. the chaining example will take 8 seconds to output all four names.

The order of output in the array is strictly the same as the order of input promises to Promise.all.

🚨 Even if there is a single failure in Promise.all, the result will be that rejection or failure.

const waitForMe = function (name) {
    return new Promise((resolve, reject) => {
        if (name === "Robert") {
            return reject("Robert is always on time");
        } else {
            setTimeout(() => {
                return resolve(name);
            }, 2000);
        }
    });
}

const firstPromise = waitForMe("Parwinder");
const secondPromise = waitForMe("Lauren");
const thirdPromise = waitForMe("Robert");
const fourthPromise = waitForMe("Eliu");

Promise.all([firstPromise, secondPromise, thirdPromise, fourthPromise])
    .then((data) => {
        console.log(data);
    })
    .catch((error) => {
        console.log(error); // Robert is always on time
    })
Enter fullscreen mode Exit fullscreen mode

It will ignore all the other successfully resolved promises. If there is more than one rejection, it will output the rejection from a promise that comes first in the input array of promises.

const waitForMe = function (name) {
    return new Promise((resolve, reject) => {
        if (name === "Robert") {
            return reject("Robert is always on time");
        } else if (name === "Lauren") {
            return reject("Lauren is always on time");
        } else {
            setTimeout(() => {
                return resolve(name);
            }, 2000);
        }
    });
}

const firstPromise = waitForMe("Parwinder");
const secondPromise = waitForMe("Lauren");
const thirdPromise = waitForMe("Robert");
const fourthPromise = waitForMe("Eliu");

Promise.all([firstPromise, secondPromise, thirdPromise, fourthPromise])
    .then((data) => {
        console.log(data);
    })
    .catch((error) => {
        console.log(error); // Lauren is always on time
    })
Enter fullscreen mode Exit fullscreen mode

Promise.race

Promise.race handles a unique case. When you would like to run multiple async operations at the same time, but not wait for all to complete. Instead, you want to execute callback as soon as the first one completes (hence the keyword "race").

const waitForMe = function (name, time) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            return resolve(name);
        }, time);
    });
}

const firstPromise = waitForMe("Parwinder", 4000);
const secondPromise = waitForMe("Lauren", 3000);
const thirdPromise = waitForMe("Robert", 7000);
const fourthPromise = waitForMe("Eliu", 5000);

Promise.race([firstPromise, secondPromise, thirdPromise, fourthPromise])
    .then((data) => {
        console.log(data); // Lauren
    })
    .catch((error) => {
        console.log(error);
    })
Enter fullscreen mode Exit fullscreen mode

I have made the setTimeout time an argument as well. With each name, I am passing the time. "Lauren" has the least time of 3 seconds (3000 ms) so she would always win the race, and the console outputs her name.

Discussion (2)

Collapse
alpersunter profile image
Alper SΓΌnter • Edited on

Hi. I liked the post. Just want to bring something to your attention. In the last example, (where you "race"d promises), the console log is Lauren, but it comes from "then" block, not from "catch" block. I tested it on JSFiddle before posting jsfiddle.net/ma26yrhj/. Nice post overall, just wanted to mention this. Have a nice day. (edit: typo)

Collapse
bhagatparwinder profile image
Parwinder πŸ‘¨πŸ»β€πŸ’» Author

Thanks for pointing it out. Updated!