Promise anti-pattern

This post is 4 years old. (Or older!) Code samples may not work, screenshots may be missing and links could be broken. Although some of the content may be relevant please take it with a pinch of salt.

If you are working with JavaScript promises it's likely that you have noticed before that there are two ways that you can catch promise rejections - either using the .catch() method or by passing in an error handler function to a .then() statement.

Passing in a second argument to .then() is considered to be a promise anti-pattern. And there's a good reason for this.

In order to better understand this anti-pattern we are going to create a sample function that mimics how an API would work. Let's assume that we have a function that returns profile information of a user:

function getProfile() {
const person = {
name: 'dave',
age: 22,
address: {
city: 'London',
},
};
return new Promise((resolve, reject) => {
if (!person) {
reject();
}
resolve(person);
});
}

The function above essentially returns the person object that is specified within the scope of the function itself. What we are mimicing here is a call to an API that returns us the object.

Handling errors with .then()

Let's see what happens to our code when we want to capture the errors using the callback pattern - whereby we are passing in a success and an error handler to a .then() statement:

getProfile().then(
(response) => {
delete response.address;
console.log(response.address.city);
},
(error) => console.log(`Scary error happened: ${error}`)
);

Also notice that we are deliberately removing the address property from the person object - in other words we are deliberately generating an error using the console.log() statement after. As a second parameter we pass in an error handler to our .then() function which is a simple log statement that should display us our error.

Running this example will cause a few surprises. First of all we will never get the 'Scary error happened' message displayed to us. Instead we will get the message stating that we have an unhandled promise rejection: UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): TypeError: Cannot read property 'city' of undefined.

Now this is strange because we have an error handler, right? Well, it turns out that this is a wrong assumption. The error generated in the success handler part of the .then() statement is not captured by the error handler. This error handler can only catch errors that getProfile() would generate. Let's mimic this by changing getProfile():

function getProfile() {
const personx = {
name: 'dave',
age: 22,
address: {
city: 'London',
},
};
return new Promise((resolve, reject) => {
if (!person) {
reject();
}
resolve(person);
});
}

(Notice that now we have a personx object which should generate the reject(); method to be called for our promise.)

In this case, re-running the previous example, the error handler will act as expected and display the following message: Scary error happened: ReferenceError: person is not defined.

Handling errors using .catch()

In order to avoid nasty surprises we need to make sure that we use .catch() after our .then() statement. The catch statement will not only capture errors that happened with the request but it will also capture errors that happened in the .then() block itself:

getProfile()
.then((response) => {
delete response.address;
console.log(response.address.city);
})
.catch((error) => console.log(`Scary error happened: ${error}`));

We still see the Scary error happened: ReferenceError: person is not define message. Now let's go back and change personx to be person again and run our example. This time we don't get an unhandled promise rejection warning but instead we see our error message: Scary error happened: TypeError: Cannot read property 'city' of undefined.

Summary

To sum it up please refer to this pseudo-code:

successfulPromiseRequest()
.then((response) => {
// errors here are captured by .catch()
})
.catch((error) => console.log(error));

successfulPromiseRequest().then(
(response) => {
// errors here will go unhandled
},
(error) => console.log(error)
);

failedPromiseRequest()
.then((response) => {
// errors for the request are captured by .catch()
})
.catch((error) => console.log(error));

failedPromiseRequest().then(
(response) => {
// errors for the request will be handled
},
(error) => console.log(error)
);

Since you can have multiple .then() statments when workin with Promises remember that .catch() is able to catch errors happening in any of the .then() statements:

successfulPromiseRequest()
.then((response) => {
return failedSuccessfulPromiseRequest(); //will be caught by catch()
})
.then((response) => {
return successfulPromiseRequest();
})
.catch((error) => console.log(error));