ES7 Async/Await pitfalls

What is async/await?

ECMAScript 6 introduced Promises to simplify program flow. However, when using promises we still need callbacks and it’s not always clear what the program does. For example, This code snippet:

function doSomething() {
let i = 0;
waitOneSecond() // Returns a promise
.then(() => console.log(i));
i = 5;
}

Will print 5, and not 0.

For this reason, ES7 introduced async functions. The previous example can be rewritten like this:

async function doSomething() {
let i = 0;
await waitOneSecond();
console.log(i);
i = 5;
}

This snippet prints the number 0. The await keyword stops the execution of the current function until the promise is resolved, and yields its return value. Promise rejections are handled with try/catch blocks, but this will have to wait for another post.

Common pitfalls

Loss of efficiency

While async/await does not affect performance, it can lead to inefficient design patterns. For example, suppose we want to fetch some users from a database and get an average of their ages. We’ll do it like this:

async function getUserAge(userId) {
await waitOneSecond();
return 7; /* Good enough for now */
}
async function getAverageAge(userIds) {
let sumOfAges = 0;
let numOfUsers = userIds.length;
for (let userId of userIds) {
sumOfAges += await getUserAge(userId);
}
return sumOfAges / numOfUsers;
}

Right?

Wrong.

Suppose we have 5 users. Then this function will loop through the users and await each one of their database calls separately, which will take a second per user. So, overall, 5 seconds. Instead, let’s perform all of the calls in parallel:

async function getAverageAge(userIds) {
let sumOfAges = 0;
let numOfUsers = userIds.length;
    // Maps userId to promise of user's age
let agesPromises = userIds.map(getUserAge);
let ages = await Promise.all(agesPromises);
    for (let age of ages) {
sumOfAges += age;
}
return sumOfAges / numOfUsers;
}

This is a little more complicated, but this time all of the calls are performed at the same time! This function will only take about 1 second, no matter how many users you have. This is something to keep in mind when using async functions.

State modification

While async functions are much easier to read, they don’t magically make your code synchronous; they’re just syntactic sugar for plain promises. The usual pitfalls still apply. Suppose we want to give every new user an id:

let currentUserId = 0;
async function getInfoAboutUser() {
currentUserId++; // Unique id for each user
await waitTenSeconds(); // Get some other data
return { id: currentUserId };
}
async function registerUser() {
let user = await getInfoAboutUser();
await storeUser(user);
}

Now, imagine 2 different users register one right after the other. getInfoAboutUser will be executed at the same time for both users, and when the 10 seconds are over, both will get an id equal to 2.

In this example, it’s rather easy to fix the problem:

async function getInfoAboutUser() {
await waitTenSeconds(); // Get some other data
currentUserId++; // Unique id for each user
return { id: currentUserId };
}

But it can be trickier sometimes.

Conclusion

Async functions are a very useful and welcome feature in my opinion, but we need to remember that they aren’t magic.

I hope you enjoyed reading this post and found it useful. If there are any other pitfalls worth mentioning, or you have any questions, please let me know in the comments :)