Common Javascript Promise mistakes every beginner should know and avoid

I wish I knew about all these mistakes while learning javascript and promises.

Common Javascript Promise mistakes every beginner should know and avoid

Whenever some new developer says that their code is not working or complain about the execution speed, I always check for these mistakes first. When I started coding 4 years back, I didn't know about this and I used to ignore them. But after being assigned to a project which handles around a million requests in a few minutes, this was no longer a case. I had no other option but to optimize the code (Because we'd reached the level where vertical scaling was no longer possible).

So in this article, I would like to talk about these common mistakes everyone makes and usually ignores. And also there are some possible solutions to these mistakes.


Mistake #1: Using try/catch inside the Promise block

It's redundant to use try/catch inside a Promise block (btw, this contradicts the Mistake #2). If your code throws an error inside the Promise block, it'll automatically be handled by the catch scope.

// This is redundant
new Promise((resolve, reject) => {
  try {
    const data = someFunction();
    // your logic
    resolve();
  } catch (e) {
    reject(e);
  }
})
  .then(data => console.log(data))
  .catch(error => console.log(error));

Instead, let the code throw an error and handle it outside your Promise like this:

// Instead try this
new Promise((resolve, reject) => {
  const data = someFunction();
  // your logic
  resolve()
})
  .then(data => console.log(data))
  .catch(error => console.log(error));

This will always work except in the case below.

Mistake #2: Using async function inside your Promise block

There are some side effects of an async function inside a Promise block

Now, let's say in your Promise block, you want to perform some async task and you add the `async` keyword and your code might throw an error. But now, you cannot handle this error even if there is a .catch() block or you await your promise inside a try/catch block.

// This code can't handle this error
new Promise(async () => {
  throw new Error('message');
}).catch(e => console.log(e.message));


// This will also fail to handle the error
(async () => {
  try {
    await new Promise(async () => {
      throw new Error('message');
    });
  } catch (e) {
    console.log(e.message);
  }
})();

Whenever I see async functions inside a Promise block, I always try to separate the async logic outside the Promise block to make that block synchronous. And this works 9/10 times. But still, for some cases, you might need an async function. In that case, you don't have any other option but to handle that manually by try/catch block.

new Promise(async (resolve, reject) => {
  try {
    throw new Error('message');
  } catch (error) {
    reject(error);
  }
}).catch(e => console.log(e.message));


// Or using async/await
(async () => {
  try {
    await new Promise(async (resolve, reject) => {
      try {
        throw new Error('message');
      } catch (error) {
        reject(error);
      }
    });
  } catch (e) {
    console.log(e.message);
  }
})();

Mistake #3: Forgetting .catch() block

Typically, this is a mistake you generally don't know you're making until some of your test cases fail. Or if you're a kind of test-atheist who doesn't believe in writing tests, this code will definitely crash in production. Because the production environment strictly follows Murphy's law.

"Anything that can go wrong will go wrong" - Murphy's law

To make your code more elegant, you can await your promise in the try/catch block instead of using .then().catch().

Mistake #4: Not using Promise.all()

Promise.all() is your friend

If you're a pro developer, you already know what I'm going to talk about here. If you have multiple promises which are independent of each other, you can resolve all of them concurrently. By default, the Promises are concurrent in nature but If you're awaiting them one by one it will take too much time. Promise.all() will save you a lot of waiting time.

const { promisify } = require('util');
const sleep = promisify(setTimeout);

async function f1() {
  await sleep(1000);
}

async function f2() {
  await sleep(2000);
}

async function f3() {
  await sleep(3000);
}

// Running code sequentially
(async () => {
  console.time('sequential');
  await f1();
  await f2();
  await f3();
  console.timeEnd('sequential'); // Approx 6 seconds
})();

This code takes around 6 seconds to execute. Now if we replace the code with Promise.all(), it will take around 3 seconds.

(async () => {
  console.time('concurrent');
  await Promise.all([f1(), f2(), f3()]);
  console.timeEnd('concurrent'); // Approx 3 seconds
})();

Mistake #5: Using Promise.race() without complete understanding.

It doesn't necessarily make your code faster

This might seem a little controversial statement but it's true. I'm not saying that Promise.race() is useless but you must know why you're using this.

This is useful if you want to run your code immediately after any one of your Promises resolves. But that doesn't mean that it will exit the code immediately after that. It will wait until all the promises get resolved and only after that, it will release the thread. So make decisions wisely.

const { promisify } = require('util');
const sleep = promisify(setTimeout);

async function f1() {
  await sleep(1000);
}

async function f2() {
  await sleep(2000);
}

async function f3() {
  await sleep(3000);
}

(async () => {
  console.time('race');
  await Promise.race([f1(), f2(), f3()]);
})();

process.on('exit', () => {
  console.timeEnd('race'); // Approx 3 seconds
});

As you can see in the code above, it will take 3 seconds to exit instead of 1 second.

Mistake #6: Overuse of Promises

They make your code slower; don't overuse them

We generally see developers using a long chain of .then() just to make their code look elegant. You won't notice any difference until this chain is too long. If you want to see this, run your code with this command and open this link: chrome://tracing/ (works only for Chrome)

node --trace-events-enabled fileName.js

Code with unnecessary .then() blocks

new Promise((resolve) => {
  // Some code which returns userData
  const user = {
    name: 'John Doe',
    age: 50,
  };
  resolve(user);
}).then(userObj => {
  const { age } = userObj;
  return age;
}).then(age => {
  if (age > 25) {
    return true;
  }
  throw new Error('Age is less than 25');
}).then(() => {
  console.log('Age is greater than 25');
}).catch(e => {
  console.log(e.message);
});

If you open the log file in chrome's tracing tool, you'll see multiple green blocks labeled as Promise. And if you click any of them, you'll see they all take a few milliseconds to execute. Hence, the more these promises blocks, the more time it will take to execute.

Code with single .then() containing all the synchronous statements

new Promise((resolve, reject) => {
  // Some code which returns userData
  const user = {
    name: 'John Doe',
    age: 50,
  };
  if (user.age > 25) {
    resolve();
  } else {
    reject('Age is less than 25');
  }
}).then(() => {
  console.log('Age is greater than 25');
}).catch(e => {
  console.log(e.message);
});

Here, there are fewer green Promise blocks. And if you increase the .then() chain, you'll notice a huge difference in time taken by both the codes.

The solution is, not to resolve multiple promises with synchronous code. Synchronous code should reside in a single block. Only use this multiple times if you want to execute some asynchronous code.


Footnotes

If you like this blog and want to see the blogs like this in future, please subscribe to our blog. We hate spam, we'll never spam your inbox. Also, let me know if there is any error or any suggestion in this blog. I would love to learn something new.