Common Promise Anti-Patterns

Common Promise Anti-Patterns

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 the return keyword before fetch(...).

      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 the catch method to the then 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:

  1. getData function is invoked.

  2. Promise.reject(new Error()) creates a rejected promise.

  3. As a result of promise rejection, the callback function of the catch method is invoked.

  4. "inside catch block in getData function" gets logged on the console.

  5. As the callback function of the catch method didn't explicitly return anything, the callback function implicitly returns undefined.

  6. The promise returned by the catch method fulfills with the return value of its callback function, i.e. undefined.

  7. This fulfilled promise is returned by the getData function to its calling code.

  8. As the promise returned by the getData function fulfilled with the value of undefined, 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.

Did you find this article valuable?

Support Yousaf Khan by becoming a sponsor. Any amount is appreciated!