Promise anti-pattern
Older Article
This article was published 9 years ago. Some information may be outdated or no longer applicable.
If you’re working with JavaScript promises, you’ve probably noticed there are two ways to catch promise rejections: the .catch() method, or passing an error handler function to .then().
Passing a second argument to .then() is considered a promise anti-pattern. And there’s a good reason for it.
To see why, let’s create a sample function that mimics an API call. It 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 returns the person object defined within its own scope. We’re mimicking a call to an API that hands us back an object.
Handling errors with .then()
Let’s see what happens when we try to capture errors using the callback pattern, passing both a success and an error handler to .then():
getProfile().then(
(response) => {
delete response.address;
console.log(response.address.city);
},
(error) => console.log(`Scary error happened: ${error}`)
);
Notice we’re deliberately deleting the address property from the person object, then trying to access it. We’re manufacturing an error on purpose. The second parameter is our error handler, a simple log statement that should show us the error.
Running this produces some surprises. You’ll never see the ‘Scary error happened’ message. Instead you’ll get an unhandled promise rejection: UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): TypeError: Cannot read property 'city' of undefined.
That’s odd, because we’ve got an error handler right there. But here’s the catch (pun intended): the error generated in the success handler part of .then() is not captured by the error handler. That error handler can only catch errors that getProfile() itself generates. Let’s prove it by breaking getProfile():
function getProfile() {
const personx = {
name: 'dave',
age: 22,
address: {
city: 'London',
},
};
return new Promise((resolve, reject) => {
if (!person) {
reject();
}
resolve(person);
});
}
(Now we’ve got a personx object, which means person is undefined and reject() fires.)
Re-running the previous example, the error handler kicks in as expected: Scary error happened: ReferenceError: person is not defined.
Handling errors using .catch()
To avoid nasty surprises, use .catch() after your .then(). The catch statement captures errors from the request and errors that happened inside 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 change personx back to person and run the example again. This time there’s no unhandled promise rejection warning. Instead we get our error message: Scary error happened: TypeError: Cannot read property 'city' of undefined.
Summary
Here’s the pseudo-code breakdown:
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 chain multiple .then() statements with Promises, remember that .catch() catches errors from any of them:
successfulPromiseRequest()
.then((response) => {
return failedSuccessfulPromiseRequest(); //will be caught by catch()
})
.then((response) => {
return successfulPromiseRequest();
})
.catch((error) => console.log(error));