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! 💫

Master Asynchronous JavaScript 🚀

Learn how to write modern and easy-to-read asynchronous code with a FREE 5-day email course.

Through visual graphics you will learn how to decompose async code into individual parts and put them back together using a modern async/await approach. Moreover, with 30+ real-world exercises you'll transform knowledge into a practical skill that will make you a better developer.

Refactoring Callbacks, a FREE 5-day email course. 30+ real-world exercises, a visual guide and 5 days, 5 lessons.

Get Lesson 1 now 👇🏼

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

14 Linting Rules To Help You Write Asynchronous Code in JavaScript

A compiled list of linting rules to specifically help you with writing asynchronous code in JavaScript and Node.js.
Read article

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

Run Concurrent Tasks With a Limit Using Pure JavaScript

Run asynchronous tasks with a concurrency limit without having to rely on an external library.
Read article