Red and Blue Function Mistakes in JavaScript
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.
Not awaiting immediately
Async code allows for parallelization, so it's tempting to kick off some work without awaiting it so that we can kick off all the work we want to do:
const fruitPromise = Fruit.findById(fruitId);
const veggiePromise = Veggie.findById(veggieId);
try {
const veggie = await veggiePromise;
const fruit = await fruitPromise;
} catch (err)...
This is a common mistake! If fruitPromise
rejects before veggiePromise
resolves, as far as Node knows, fruitPromise
isn't in a catch block and hasn't had its catch registered, so it will emit an unhandledRejection event. We instead need to write our parallel code in a way where promises are awaited immediately.
try {
const [fruit, veggie] = await Promise.all([Fruit.findById(fruitId), Veggie.findById(veggieId)]);
} catch (err)...
Here's some code you can run in your browser console to test this out:
function delay (ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
window.onunhandledrejection = (event) => {
console.error("unhandledrejection", event);
}
(async function main () {
const p1 = delay(1000);
const p2 = delay(1).then(() => {
throw new Error("uh oh");
})
try {
await p1;
await p2;
} catch (err) {
console.error("The error will be caught, but you'll also see an unhandled rejection logs up above", err);
}
})();
Array.filter and other methods only take blue functions
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... can easily cause race conditions
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. forEach
is mostly equivalent to a for
loop, but not when it comes to await
.
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.