Here's a fun bug I recently encountered... Let's say we have this async JavaScript function:

const isBroken = async () => {
  return false;
}

if (isBroken()) {
  throw new Error("Oopsie woopsie");
}

I've kept the code simple for this post so it's easier to analyze. In reality, you'd probably fetch data or perform some other async operation:

const isBroken = async () => {
  const result = await someAsyncOperation();
  return result !== 'the expected value';
}

if (isBroken()) {
  throw new Error("Oopsie woopsie");
}

Either way, does the body of the if statement execute? Take a second to think this through. You can check if you're right by copying the first code sample and executing it in your dev tools.

(You can probably already guess the answer, though, just based on the fact that I'm writing this post.)

Async Functions, Booleans, and Truthiness: A Slippery Bug

The if statement in this code will always execute its body, regardless of what the async function returns. ESLint won't flag this as an error. TypeScript technically should, but it has not yet implemented this warning for un-awaited async functions.

To understand why the expression always evaluates to true, recall that async/await is just syntactic sugar for Promises. In JavaScript, an async function actually wraps its return value in a Promise object—even if it seems like the function is directly returning a value, and even if the function does not await anything.

We can verify this by logging the function call:

> console.log(isBroken())
Promise {<fulfilled>: false}

Our async function's return value is not false itself but rather a Promise object that resolved with the value false. In other words, it's the same as doing this:

const isBroken = () => {
  return Promise.resolve(false);
}

if (isBroken()) {
  throw new Error("Oopsie woopsie");
}

Spot the problem? This is an issue of truthiness: a Promise object is not one of the eight falsy values in JavaScript, so checking its truthiness is pointless: When coerced to a boolean, a Promise is always true.

For the code above to work as intended, you'll need to await the result in another async function (or, equivalently, chain a .then call on the returned Promise object):

const isBroken = async () => {
  return false;
}

const main = async () => {
  const somethingIsWrong = await isBroken();

  if (somethingIsWrong) {
    throw new Error("Oopsie woopsie");
  }
}

Basic tests will catch this bug before you ever need to go looking for it yourself. But if you don't have any in place, it could easily slip right past you unless you remember that the function is async.

Attributions

Social media preview: Photo by STIL (Unsplash).