A deep understanding of javascript promises is essential to writing modern asynchronous javascript code that is free from subtle bugs and works as intended.
In this article, we will go over some common promise-related anti-patterns that should be avoided. Following is a list of anti-patterns we will discuss:
Unnecessary use of Promise Constructor
One of the most common mistakes by javascript developers, especially those who don't have much experience with promises, is creating promises unnecessarily using the promise constructor function.
Let's take a look at an example:
function fetchData(url) {
return new Promise((resolve, reject) => {
fetch(url)
.then(res => res.json(res))
.then(resolve)
.catch(reject);
});
}
The above code will work correctly if you pass a URL to fetchData
function and then wait for the promise to resolve BUT the use of the promise constructor is unnecessary.
fetch()
function already returns a promise, so instead of wrapping the fetch(...)
call with the promise constructor, we can re-write the above function as shown below:
function fetchData(url) {
return fetch(url)
.then(res => res.json(res));
}
The revised version of the fetchData
function is free from the creation of any unnecessary promise and allows the code that calls the fetchData
function to catch and handle any errors. Older version of fetchData
function also allowed the calling code to handle errors BUT the revised version does it without using the catch
method call.
Unnecessary use of the promise constructor can lead to another problem; if we forget to add the catch
method call to the promise chain inside the promise constructor, then any error thrown during the HTTP request won't be caught.
Incorrect Error Handling
When writing code that uses promises, one of the most important rules to keep in mind: either catch and handle the error or return the promise to allow the calling code to catch and handle them.
Keeping this fundamental rule in mind can help you avoid hidden bugs in the code that uses promises.
Let's take a look at an example of incorrect handling of errors that breaks the above rule:
function getData(url) {
fetch(url)
.then(response => response.json());
}
getData('https://jsonplaceholder.typicode.com/todos/1')
.then(data => console.log(data))
.catch(error => console.log(error));
The above code throws an error because the fetchData
function doesn't return the promise. It also doesn't allow the calling code to do any kind of error handling.
There are two ways to fix the above code:
Return the promise from the
fetchData
function by adding thereturn
keyword beforefetch(...)
.function getData(url) { return fetch(url) .then(response => response.json()); }
As the above function just makes the HTTP request and returns the response data after calling the .json() method on the response object, the calling code is responsible for getting and using the response data as well as handling the errors.
getData(/* some url */) .then(data => { /* do something with the data */ }) .catch(error => { /* handle error */ });
Handle the error inside the
fetchData
function by chaining thecatch
method to thethen
method.function getData(url) { fetch(url) .then(response => response.json()) .then(data => { /* do something with the data */ }) .catch((err) => { /* error handling code */ }); }
and you call this function as shown below:
getData(/* some url */);
Converting Promise Rejection into Fulfillment
Each method on Promise.prototype object returns a new promise. If we are not careful, we can write code that can implicitly convert promise rejection into promise fulfillment.
Let's take a look at an example:
function getData(url) {
return Promise.reject(new Error())
.catch((err) => {
console.log("inside catch block in getData function");
});
}
getData()
.then(data => console.log("then block"))
.catch(error => console.log("catch block"));
What output do you expect?
Click here to see the output
"inside catch block in getData function""then block"
We called Promise.reject(...)
inside getData
function, so instead of logging "then block", why didn't "catch block" got logged? Instead of the catch
block, why did the callback function of the then
method was invoked?
As mentioned before, both then
and catch
methods return a new promise. Keeping this in mind, let's understand how the above code executes:
getData
function is invoked.Promise.reject(new Error())
creates a rejected promise.As a result of promise rejection, the callback function of the
catch
method is invoked."inside catch block in getData function"
gets logged on the console.As the callback function of the
catch
method didn't explicitly return anything, the callback function implicitly returnsundefined
.The promise returned by the
catch
method fulfills with the return value of its callback function, i.e.undefined
.This fulfilled promise is returned by the
getData
function to its calling code.As the promise returned by the
getData
function fulfilled with the value ofundefined
,then
method's callback is invoked in the calling code which logs"then block"
.
See this stackoverflow post which explains this behavior in more detail.
Although the above code is a contrived example, imagine if there was a fetch(...)
call instead of Promise.reject(...)
in the getData
function; if the HTTP request is successful, our code will work without any problem BUT if the HTTP request fails, the catch
method in the getData
function will convert promise rejection to promise fulfillment. As a result, instead of returning a rejected promise, the getData
function will return a fulfilled promise.
If you are wondering why the promise returned by the catch
method got fulfilled instead of getting rejected, then the answer is that the promise returned by the then
or the catch
method gets fulfilled if the callback function of the then
or the catch
method explicitly or implicitly returns a value instead of throwing or returning a promise that gets rejected.
So, how can we fix the above code example to avoid this problem?
There are two ways to fix this problem:
- Throw the error from the callback function of the
catch
method.
function getData(url) {
return Promise.reject(new Error())
.catch((err) => { throw err; });
}
This will reject the promise returned by the catch
method and this rejected promise will be returned by the getData
function. As a result, as expected, catch
block in the calling code will be invoked.
- Remove the
catch
method call.
function getData(url) {
return Promise.reject(new Error());
}
This will also invoke the catch
block in the calling code because now getData
function returns what Promise.reject(...)
returns and as mentioned before, Promise.reject()
returns a rejected promise.
I recommend using this approach instead of throwing the error from the catch
block. Just allow the calling code to catch and handle the errors. The catch
block that just re-throws the error is unnecessary.
Passing an async
function to the Promise Constructor
When creating a new promise using the promise constructor function, we pass a function to the promise constructor. This function is known as the executor function.
The executor function should never be an async function. Why is that?
If the executor function is async, any errors thrown by the async executor function will not be caught and this error won't cause the newly-constructed promise to reject. This will lead to subtle bugs that will be hard to debug.
const p = new Promise(async (resolve, reject) => {
throw new Error("error");
});
p.catch(e => console.log(e.message));
In the above code example, as the executor function is async
, the error thrown inside it doesn't reject the newly-created promise p
. As a result, the callback function of the catch
method, called on p
, never gets called.
If the executor function is synchronous, then any error thrown inside the executor function will automatically reject the newly-created promise. Try removing the async
keyword in the above code example and observe the output.
Another thing to note is that if you find yourself using await inside the executor function, this should be a signal to you that you don't need the promise constructor at all (remember the first anti-pattern discussed above).
Summary
In this article, we discussed some common promise-related anti-patterns that should be avoided.
Keep the following points in mind when using promises:
Don't explicitly create a promise just to wrap it around the code that already returns a promise.
Either handle the error or return the promise to allow the calling code to handle the error.
Keep in mind that the promise returned by the
catch
method can get fulfilled if its callback function doesn't throw the error.Never mark the executor function as
async
.