Search⌘ K
AI Features

Fundamentals of Chaining Promises

Explore how to chain multiple JavaScript promises to handle asynchronous operations in sequence. Learn to manage errors centrally with catch handlers that act like try-catch statements, ensuring robust asynchronous code. This lesson helps you write cleaner and more maintainable promise-based code.

We'll cover the following...

Introduction

Promises may seem like little more than an incremental improvement over using some combination of a callback and the setTimeout() function, but there is much more to promises than meets the eye. More specifically, there are several ways to chain promises to accomplish more complex asynchronous behavior.

Chaining promises
Chaining promises

Each call to then(), catch(), or finally() actually creates and returns another promise. This second promise is settled only once the first has been fulfilled or rejected. Consider this example:

Javascript (babel-node)
const promise = Promise.resolve(66);
promise.then(value => {
console.log(value);
}).then(() => {
console.log("Finished");
});

The call to promise.then() returns a second promise on which then() is called. The second then() fulfillment handler is called only after the first promise has been resolved. If we unchain this example, it looks like this:

Javascript (babel-node)
const promise1 = Promise.resolve(77);
const promise2 = promise1.then(value => {
console.log(value);
});
promise2.then(() => {
console.log("Finished");
});

In this unchained version of the code, the result of promise1.then() is stored in promise2, and then promise2.then() is called to add the final fulfillment handler. The call to promise2.then() also returns a promise. This example doesn’t use that promise.

Catching errors

Promise chaining allows us to catch errors that may occur in a fulfillment or rejection handler from a previous promise. For example, we have the following:

Javascript (babel-node)
const promise = Promise.resolve(88);
promise.then(value => {
throw new Error("Oops!");
}).catch(reason => {
console.error(reason.message); // "Oops!"
});

In this code, the fulfillment handler for the promise throws an error. The chained call to the catch() method, which is on a second promise, is able to receive that error through its rejection handler. The same is true if a rejection handler throws an error:

Javascript (babel-node)
const promise = new Promise((resolve, reject) => {
throw new Error("Uh oh!");
});
promise.catch(reason => {
console.log(reason.message); // "Uh oh!"
throw new Error("Oops!");
}).catch(reason => {
console.error(reason.message); // "Oops!"
});

Here, the executor throws an error that triggers the promise’s rejection handler. That handler then throws another error that is caught by the second promise’s rejection handler. The chained promise calls are aware of errors in other promises in the chain.

We can use this ability to catch errors through a promise chain to effectively act like a try-catch statement. Consider using fetch() to retrieve some data and catch any errors that occur:

import fetch from "node-fetch";
const promise = fetch("https://www.educative.io/udata/1kZPll2Qgkd/book.json");

promise.then(response => {
    console.log(response.status);
}).catch(reason => {
    console.error(reason.message);
});
A promise chain with fetch()

This example will output the response status from the fetch() call if it succeeds and will output the error message if the call fails. We can take this a step further and handle status codes outside of the 200–299 range as errors by checking the response.ok property and throwing an error if it is false, as in this example:

import fetch from "node-fetch";
const promise = fetch("https://www.educative.io/udata/1kZPll2Qgkd/book.json");

promise.then(response => {
    if (response.ok) {
        console.log(response.status);
    } else {
        throw new Error(`Unexpected status code: ${
            response.status
        } ${response.statusText}`);
    }
}).catch(reason => {
    console.error(reason.message);
});
A promise chain with fetch() catching rejections

The chained catch() call in this example creates a rejection handler that catches both errors returned by fetch() and also any errors thrown in the fulfillment handler. So instead of needing two different handles for catching the two different types of errors, we can use one to handle all of the errors that may occur in the chain.

Note: Always have a rejection handler at the end of a promise chain to ensure that you can properly handle any errors that may occur.