Understanding error handling in Promise chains

joeattardi

Joe Attardi

Posted on September 13, 2023

Understanding error handling in Promise chains

Promise chains

You can create a chain of Promises by returning new Promises from a then handler. Here's a simple example that chains 3 promises together:

Promise.resolve(1)
    .then(id => {
        console.log(`Success: ${id}`);
        return Promise.resolve(2);
    }).then(id => {
        console.log(`Success: ${id}`);
        return Promise.resolve(3);
    }).then(id => {
        console.log(`Success: ${id}. Done!`);
    });
Enter fullscreen mode Exit fullscreen mode

The output of this chain will be:

Success: 1
Success: 2
Success: 3. Done!
Enter fullscreen mode Exit fullscreen mode

To handle any errors that may occur in the chain, you can add a call to catch at the end of the chain. If any of the Promises are rejected, this catch handler will run, and the rest of the chain is skipped.

Promise.resolve(1)
    .then(id => {
        console.log(`Success: ${id}`);
        return Promise.reject(2);
    }).then(id => {
        console.log(`Success: ${id}`);
        return Promise.resolve(3);
    }).then(id => {
        console.log(`Success: ${id}. Done!`);
    }).catch(id => {
        console.log(`Error: ${id}`);
    });
Enter fullscreen mode Exit fullscreen mode

The output of this chain will be:

Success: 1
Error: 2
Enter fullscreen mode Exit fullscreen mode

The weird parts

If you add more then calls after the catch, they will run!

Promise.resolve(1)
    .then(id => {
        console.log(`Success: ${id}`);
        return Promise.reject(2);
    }).then(id => {
        console.log(`Success: ${id}`);
        return Promise.resolve(3);
    }).then(id => {
        console.log(`Success: ${id}. Done!`);
    }).catch(id => {
        console.log(`Error: ${id}`);
    }).then(() => {
        console.log('Another then!');
    });
Enter fullscreen mode Exit fullscreen mode

You'll get this output:

Success: 1
Error: 2
Another then!
Enter fullscreen mode Exit fullscreen mode

Why does the chain continue after the catch? As it turns out, you can return another Promise from a catch handler. Here, the catch handler just prints to the console. The handler function, then, returns undefined. This actually returns a new Promise, fulfilled with the value undefined. You can verify this by adding the id argument to the last then:

Promise.resolve(1)
    .then(id => {
        console.log(`Success: ${id}`);
        return Promise.reject(2);
    }).then(id => {
        console.log(`Success: ${id}`);
        return Promise.resolve(3);
    }).then(id => {
        console.log(`Success: ${id}. Done!`);
    }).catch(id => {
        console.log(`Error: ${id}`);
    }).then(id => {
        console.log(`Success: ${id}`);
    });
Enter fullscreen mode Exit fullscreen mode

And the output:

Success: 1
Error: 2
Success: undefined
Enter fullscreen mode Exit fullscreen mode

Re-rejecting

This is a contrived scenario, but consider a function that does some asynchronous work and returns a Promise. Maybe it's a function that wraps the Fetch API, to return the JSON content. It has a catch for centralized request error logging:

function getJSON(url) {
    return fetch(url)
        .then(response => response.json())
        .catch(error => console.log('Fetch error:', error));
}
Enter fullscreen mode Exit fullscreen mode

What happens if there's an error with the Fetch call? Before reading this post, the result might surprise you!

getJSON('http://invalid.fake')
    .then(data => console.log('Success!', data))
    .catch(() => console.log('Error!'));
Enter fullscreen mode Exit fullscreen mode

Logically you might expect that Error! will be printed. But what actually happens is that getJSON logs the Fetch error but returns a fulfilled Promise. Your then handler will be executed and print:

Success! undefined
Enter fullscreen mode Exit fullscreen mode

In order to get the result you want, the catch handler inside getJSON has to return a rejected Promise. You have to "re-reject" it:

function getJSON(url) {
    return fetch(url)
        .then(response => response.json())
        .catch(error => {
            console.log('Fetch error:', error);
            return Promise.reject(error);
        });
}
Enter fullscreen mode Exit fullscreen mode

You could also throw the error, which will implicitly return a rejected Promise as well:

function getJSON(url) {
    return fetch(url)
        .then(response => response.json())
        .catch(error => {
            console.log('Fetch error:', error);
            throw error;
        });
}
Enter fullscreen mode Exit fullscreen mode

Either way, now when you call getJSON and an error occurs, it will correctly return a rejected Promise.

Summary

Promise chains actually work a lot like try/catch blocks in JavaScript. If an exception is thrown within a try block, it jumps right to the catch block and skips the rest of the try - just like how catch() skips the rest of the Promise chain.

Also, if you have a function that catches an exception, you'll need to re-throw it if you want it to propagate back up to the calling function.

💖 💪 🙅 🚩
joeattardi
Joe Attardi

Posted on September 13, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related