Skip to content

Why You Shouldn't Mix Promise.then() With Async/Await Syntax

3 min read

Have you ever gotten yourself in a tangled mess of code when working with asynchronous JavaScript? A nested callback here, a promise there, and to finish it off, a sparkle of async/await. 😱 🏃🏼‍♂️💨

I don't know about you, but when I see callbacks, promises and async/await mixed in the same function I'm horrified by the bugs that may lie underneath. 🐛

Working with and, most importantly, reading asynchronous code written in pure async/await is such a breath of fresh air!

I immediately understand what the code does and I can easily make changes if needed.

Aaaaaah. Life is good. 😌

Promise rejections !== async errors

But life wasn't that good this one time when I was working on a Node.js project and started seeing unhandled promise rejections in the production logs.

After some debugging, I eventually traced the issue to an Express middleware function with the following code. Can you tell what's wrong with it?

try {
  const rates = await getCurrencyConversions();
  Price.update({ value: rates.EUR }, { where: { description: "dollar" } })
    .then((result) => {
      return res.status(200).json({ message: "Successfully updated conversion rate.", result });
    });
} catch (error) {
  const message = "Failed to update conversion rate.";
  log.error(message, { error });
  return res.status(500).json({ message });
}

This code isn't inherently wrong. When everything works without any errors, it does what it's supposed to do.

It's when things don't go as expected that we notice strange behaviour. What do you think happens when the Price.update() function throws an error?

A: The error is caught in the catch block, logged, and a 500 status code is sent to the client
B: A global unhandledRejection event is emitted by the Node.js process

.
.
.
.
.

You can probably guess where I'm going with this, the correct answer is B.

Most people would expect that if an error is thrown in the try-block it would be caught and handled in the catch-block. In this case, however, that's far from true. When Price.update() rejects, it looks for the nearest .catch() method. If it doesn't find any, Node.js emits a global unhandledRejection event.

And making matters worse, if you're using Node.js 15 or higher and your application doesn't have an unhandledRejection event listener, your server will crash!

I could've fixed the issue by appending a .catch() method to Price.update() but then I'd end up with two places that are responsible for handling exceptions (.catch() method & catch-block). Code is easier to maintain if errors are handled in one place.

I generally recommend sticking to async/await syntax. There's no need to mix the two syntaxes because if a function returns a promise, you can also use async/await. Async/await is built on top of promises after all.

When I figured out what the source of the bug was, I refactored the code to async/await and got rid of the .then() method:

try {
  const rates = await getCurrencyConversions();
  const result = await Price.update(
    { value: rates.EUR },
    { where: { description: "dollar" } }
  );
  return res.status(200).json({ message: "Successfully updated conversion rate.", result });
} catch (error) {
  const message = "Failed to update conversion rate.";
  log.error(message, { error });
  return res.status(500).json({ message });
}

After that, the unhandled promise rejections disappeared from the logs and life was good again! 💫

Transform Callbacks into Clean Async Code! 🚀

Tired of messy callback code? Download this FREE 5-step guide to master async/await and simplify your asynchronous code.

In just a few steps, you'll transform complex logic into readable, modern JavaScript that's easy to maintain. With clear visuals, each step breaks down the process so you can follow along effortlessly.

Refactoring Callbacks Guide Preview

You'll also get tips on building scalable Node.js applications about twice a month. I respect your email privacy. Unsubscribe any time.

You might also like

A Visual Guide to Refactoring Callback Functions to Promises & Async/await

Nested callbacks can be stressful. Use this technique to confidently refactor messy callbacks into clean async/await.
Read article

Understanding Async & Await

Async/await can be intimidating. With a little guidance, you can write modern asynchronous JavaScript code that just works.
Read article

3 Reasons Why Async/Await Is Better Than Chaining Promises

Why use async/await when you can use promises instead? Here's why async/await excels at asynchronous JavaScript code.
Read article