Bob Nystrom's What Color is Your Function does an amazing job of describing why it can be painful when programming languages have different rules for calling synchronous and asynchronous functions. Promises and async/await have simplified things in JavaScript, but it's still a language with "red" (async) and "blue" (sync) functions, and I consistently see a few understandable errors from red vs. blue function confusion. Let's go through some of the most common mistakes – none of these are bad things to get wrong, they're just a symptom of how confusing this can be!
Omitting await from try/catch blocks
The most common mistake I see is omitting await
from try/catch
blocks with async
functions. The code looks reasonable, but the catch
block will only be able to catch synchronously thrown errors. To make matters worse, error handling logic is often less well tested than the happy path when everything works, which makes this pattern more likely to sneak its way into production code.
async function throwsError () {
throw new Error("alas! an error");
}
try {
return throwsError();
} catch (err) {
console.error("Oh no! This catch block isn't catching anything", err);
}
An async
function that throws is the equivalent of a Promise.reject
, and when written that way, it's a bit clearer what's going on:
try {
return Promise.reject(new Error("alas! an error"));
} catch (err) {
console.error("It's clearer that this `catch` can't catch that `Promise.reject`. This is equivalent to the earlier code");
}
Personally, I'm starting to wonder whether using try
and catch
blocks at all is a mistake when dealing with async
code. They take up space and don't offer the same pattern matching that a library like Bluebirdjs can add to catch
when you only want to catch some specific known errors: await tryThing().catch(NotFoundErrorClass, handleErrPattern)
feels substantially cleaner to me than the equivalent try/catch
block.
Array.filter(async () => false)
In recent years, JavaScript has added lots of useful Array methods like filter
, map
, forEach
, and flatMap
, and JavaScript programmers often use libraries like lodash to write functional code rather than writing for loops. Sadly, none of those Array methods or lodash helpers work with red async functions and are a common source of coding errors.
const things = [true, false, 1, 0, "", new Date("not a date") - 0];
const filteredThings = things.filter(async (thing) => thing);
How many things do we end up with in filteredThings
? Surprisingly, the answer has little to do with JavaScript type coercion: filteredThings
will be the same size as things
. An async
function returns a Promise
and even a Promise
that resolves to false is still a truthy value: Boolean(Promise.resolve(false)) === true
. If we want to do any sort of filtering using an async function, we need to switch out of blue sync mode and into red async mode.
(async function () {
// You should use a library like Bluebird rather than filtering like this! this is only for illustration
const things = [true, false, 1, 0, "", new Date("not a date") - 0];
const predicateValues = await Promise.all(things.map(async (thing) => thing));
const filteredThings = things.filter((_thing, i) => predicateValues[i]);
})();
When you see Array.filter(async (thing) => thing)
written out like that, the mistake is pretty clear. It can be harder to notice when you see code like const goodThings = things.filter(isGoodThing)
; you need to check whether isGoodThing
is red or blue.
Array.forEach(async...
We see a similar problem when people use Array.forEach
with an async function:
const fruitStatus = {};
["apple", "tomato", "potato"].forEach(async (food) => {
fruitStatus[food] = await isFruit(food);
});
return fruitStatus;
In some ways, this is a more dangerous pattern. Depending on when you check, fruitStatus
may have some, none, or all of the correct isFruit
values. If isFruit
is normally fast, problems and bugs might not manifest until isFruit
slows down. A bug that only shows up some of the time is much harder to debug than one that's always there.
Await off my shoulders
Despite how easy it is to make mistakes with async/await
, I still love it – it feels easier to work with than Promises or callbacks. Dealing with asynchronous code is still one of the harder parts of programming in JavaScript, but tools like bluebird, the TypesScript no-unnecessary-condition rule, and the eslint promise plugin can help surface these easy-to-make red/blue function mistakes early. Hopefully, seeing the mistakes we often make will help you avoid some frustrating minutes debugging.
A former teacher, Will is an engineering manager focused on database performance, team effectiveness, and site-reliability. He believes most problems can be solved with judicious use of `sed` and `xargs`.