6 Common mistakes in using JavaScript Promises

6 Common mistakes in using JavaScript Promises

As beginners of JavaScript Promises, you may make a few common mistakes. This article to help you identify them and start using them in the right way.

ยท

8 min read

Hello friends ๐Ÿ‘‹, we are reaching the end of the series, Demystifying JavaScript Promises - A New Way to Learn. So far, we have learned a lot about JavaScript asynchronous programming and promises.

If you are new to the series, please check out the previous articles,

In this article, we will look into a list of common mistakes we make in using promises.

1. Looping with Promises

The most common mistake is handling promises inside a loop(for, forEach, and all other cousin loops). We use promises to accomplish asynchronous(async) operations. Async operations take time to complete. How much time? It depends on many factors, and we can not guarantee.

So, when we handle multiple promises within a loop, we need to be a bit careful. Some promises may take longer to resolve. The rest of the code inside the loop may finish execution early and may cause undesirable behaviors. Let's understand it with a simple example.

I want to fetch a few GitHub details of my three dear friends(Savio Martin, Victoria Lo, and Usman Sabuwala) along with mine.

First, Let's create an array of their GitHub user ids.

const users = ['saviomartin', 'victoria-lo', 'max-programming', 'atapas'];

Now, let's create a simple function to call GitHub API to fetch user details by user id.

const fetchData = user => {
    return fetch(`https://api.github.com/users/${user}`);
}

So let's loop?

const loopFetches = () => {
    for (let i = 0; i < users.length; i++) {
        console.log(`*** Fetching details of ${users[i]} ***`);
        const response = fetchData(users[i]);
        response.then(response => {
            response.json().then(user => {
                console.log(`${user.name} is ${user.bio} has ${user.public_repos} public repos and ${user.followers} followers`);
            });
        });
    }
}

loopFetches();

We use the for-loop to loop through the user id array and call the fetchData() method. The fetchData() method returns a promise with a response. So we get the response value using the .then() handler method. The response value is another promise. Hence we need to invoke .then() one more time to fetch the intended data.

The fetchData() performs an asynchronous operation, and you can not ensure the sequence of output in this case. So, there are chances we get the output in a different order than the user id passed to the fetch method. Our order was savio, victoria, max-programming, and atapas. However, one possible output order could be,

for loop output

Let's fix this. Now we will change the loop function a bit to use our favorite async/await keywords. In this case, the control waits when it encounters the await keyword. Hence we have an assurance of getting the first user data and then move to the second one, then the next one, and so on.

const loopFetchesAsync = async () => {
    for (let i = 0; i < users.length; i++) {
        console.log(`=== Fetching details of ${users[i]} ===`);
        const response = await fetchData(users[i]);
        const user = await response.json();            
        console.log(`${user.name} is ${user.bio} has ${user.public_repos} public repos and ${user.followers} followers`);
    }
}

Here is the output(always),

fetch async

But, still, there is a problem! Fetching each of the user details should be an asynchronous activity. Also, these are unrelated promises, and they must run in parallel to produce a result. In the example above, the promise execution is synchronous.

To fix that, use the Promise.all([...]) or Promise.allSettled([...]) APIs. They both take an array of promises, run them in parallel, and return the result in the same order of the inputs. The total time taken by these API methods depends on the max time taken by any of the input promises. It is far better than executing them sequentially.

const loopAll = async () => {
    const responses = await Promise.all(users.map(user => fetchData(user)));
    const data = await Promise.all(responses.map(response => response.json()));
    console.log(data);
    data.map(user => {
        console.log(`*** Fetching details of ${user.name} ***`);
        console.log(`${user.name} is ${user.bio} has ${user.public_repos} public repos and ${user.followers} followers`)
    });
}

loopAll();

Promise API output array(check the order of elements in the array is the same as the input order),

promise all output array

The output we print in the browser console,

promise all output

Conclusion: Use promise APIs(.all or .allSettled) to handle multiple unrelated promise executions than using loops.

2. Promise Chain vs. No Chain

When using a promise chain, do NOT repeat the promise in front of the .then, .catch handler methods.

Let's create a promise that resolves a value of 10.

const ten = new Promise((resolve, reject) => {
    resolve(10);
});

Now, let's form a proper promise chain. Here we return and move the values down the chain.

ten
.then((result) => {
   // returns 20
    return result + 10;
})
.then((result) => {
   // returns 200
    return result * 10;
})
.then((result) => {
   // returns 190
    return result - 10;
})
.then((result) => {
  // logs 190 in console
    console.log(result);
});

So, the output we see in the console is the value 190. Now take a closer look at the code below. Here we use the promise ten in front of all the .then() methods. We are NOT forming a chain here.

ten
.then((result) => {
   // returns 20
    return result + 10;
})
ten
.then((result) => {
   // returns 100
    return result * 10;
})
ten
.then((result) => {
   // returns 0
    return result - 10;
})
ten
.then((result) => {
   // logs 10 in the console.
    console.log(result);
});

Always remember this,

chain-no-chain.png

Conclusion: Do not be confused with a NO chain over a promise chain.

3. (Not)Handling Errors with Promises

The most straightforward way to handle errors in promises is with the .catch() hander method. But when we forget to use it, we may mishandle an error scenario in our code.

Here is a simple function that takes a number as an argument. If it is an even number, it resolves by returning a string, Even. In case of an odd number, the promise rejects with an error message.

const oddEven = (num) => {
  return new Promise((resolve, reject) => {
    if (num % 2 === 0) {
      resolve("Even");
    } else {
      reject(new Error("Odd"));
    }
  });
};

First, let's pass an even number, 10.

oddEven(10).then((result) => {
    console.log(result);
});

Alright, the output is expected as Even. Now, let us pass an odd number to the function.

oddEven(11).then((result) => {
    console.log(result);
});

We will get the uncaught error,

uncaught error

As we discussed, the best way is to use the .catch() always with one or multiple .then() to handle errors.

oddEven(11).then((result) => {
    console.log(result);
}).catch((err) => {
    console.log(err.message);
});

Conclusion: Always use .catch() to handle errors even if you are very sure of no error chances!

4. Missing a function in .then() handler

You may sometimes miss using the function as a parameter of the .then() handler. Please note, the .then() method takes two callback functions as arguments. The first one is to handle the resolve case and the second one for the rejected case.

But if we miss using the callback function and use any other value instead, it doesn't give us the expected output. Can you please guess the output of the following code snippet? Will it be Hello or World?

const hello = Promise.resolve("Hello");
hello.then('World').then(result => console.log(result));

It will be Hello as the first .then() method doesn't use a function callback. The previous result just falls through.

Conclusion: Do not forget the function as an argument to the .then() handler.

5. Using Promises for Synchronous Operations

Another common mistake we make is using the synchronous(in-memory) method call inside a promise and making the program execution slow.

Consider, we have an object(a user cache) to get the user details using the email id as a key.

const cache = {
    'tapas.email.com': {
        'name': 'Tapas Adhikary',
        'blog': 'GreenRoots Blog'
    }
};

Now, check out the following function. It first finds if the user is in the cache. If not, then makes the call to fetch the data and update the cache. If it is found, just print it. The following code works, but we are delaying our decision by putting the code of user retrieval from cache inside the promise.

const getData = (email) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const userFromCache = cache[email];
            if(!userFromCache) {
                // Make the call to fetch user data
                // update cache
                console.log('Make the call and update cache');
            } else {
                console.log(`User details ${JSON.stringify(userFromCache)}`);
            }
        }, 2000);
    })
};

We can rather do this,

const getData = (email) => {
    const userFromCache = cache[email];
    if(userFromCache) {
        console.log(`User details ${JSON.stringify(userFromCache)}`);
    } else {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                console.log('Make the call and update cache');
            }, 2000);

        });
    }
};

Conclusion: Do not slow down decision-making by putting the synchronous operation inside a promise.

6. Using unnecessary try-catch with promises

Last but not least. Please don't do this. It is redundant to use the try-catch inside a promise executor function. We have .then() and .catch() to handle results and errors respectively.

// Redundant try-catch
new Promise((resolve, reject) => {
    try {
      const value = getValue();
      // do something with value  
      resolve(value);
    } catch (e) {
      reject(e);
    }
})
.then(result => console.log(result))
.catch(error => console.log(error));

Better way,

// Better
new Promise((resolve, reject) => {
    const value = getValue();
    // do something with value 
    resolve(value);
})
.then(result => console.log(result))
.catch(error => console.log(error));

Conclusion: The try-catch inside a promise is just a waste of time and effort.

That's all for now. Do not forget to have a loot at the GitHub repository with all the source code used in this article,

You can also try out some cool quizzes based on the learning from the series. Check this out.


I hope you found this article insightful. Please like/share so that it reaches others as well.

Let's connect. You can follow me on,

Did you find this article valuable?

Support Tapas Adhikary by becoming a sponsor. Any amount is appreciated!