I Bet You've Been Doing Async/Await Wrong

I Bet You've Been Doing Async/Await Wrong

Heredia, Costa Rica, 2022-12-20

Hello there! I know, you're thinking "this guy!, I've done async programming for a long time without issues". Well, it doesn't hurt double-checking, right?

Why I'm Writing This

Literally 9 out 10 (or more) tutorials about async/await, regardless of the language, only teaches how to release the thread. All those tutorials are summed up in one sentence:

If you call an asynchronous function, await it.

That's it. That's all they teach. Let's put that lesson to the test.

Making Mashed Potatoes

Imagine you are in your kitchen alone and you want to make mashed potatoes using my recipe. The process is really simple:

  1. Peel the potatoes.

  2. Dice the potatoes.

  3. Cook the potatoes until they are soft.

  4. Grate your preferred yellow cheese.

  5. Once the potatoes are soft, mash them.

  6. Mix the mashed potatoes with the cheese.

  7. Salt until you are content.

Let's write this program. I'll write it in JavaScript, but it may very well be C#. It is the same concept, and both behave the same.

Let's do it synchronously first:

peelPotatoes();
dicePotatoes();
cookPotatoes();
grateCheese();
mashPotatoes();
mixWithCheese();
addSalt();

Simple. But can you see any time wasted here? If you have really cooked, you know diced potatoes will take around 25 minutes to be soft. What are you doing while they become soft (while the call to cookPotatoes() returns)? Nothing. You are waiting. You could have grated the cheese while waiting for the potatoes. Shame on you.

So people will tell you: Make cookPotatoes() asynchronous so you are free to do anything else. Great advice! Let's make it so, and let's rename it to cookPotatoesAsync(), just for the visual cue that provides.

peelPotatoes();
dicePotatoes();
await cookPotatoesAsync();
grateCheese();
mashPotatoes();
mixWithCheese();
addSalt();

As you can see, now we have followed the advice and on top of that we have put the one lesson tutorials give you: We have awaited the call.

I invite you to debug the two versions so far. You'll discover that both codes behave exactly the same. You are still wasting the 25 minutes waiting for the potatoes to be done.

So what's wrong? The tutorials you have read, that's what!

The Correct Lesson

Asynchronous programming is not that hard, and to prove it, I'll sum up the entire lesson in one sentence:

If you call an asynchronous function, only await when you require the result.

Let's put this alleged "correct" lesson to the test.

peelPotatoes();
dicePotatoes();
const cookPromise = cookPotatoesAsync(); // It would be cookTask in .Net.
grateCheese();
// Ok, now I cannot continue unless the potatoes are cooked.
// Great, as per the correct lesson, now you may await.
await cookPromise;
mashPotatoes();
mixWithCheese();
addSalt();

If you put the above to the test, you'll notice that you grated the cheese while the potatoes were in the process of being cooked. Your code now runs faster.

Conclusion

The one great conclusion that you can take out of this is: The await keyword synchronizes. Ring a bell? Haven't you read or heard at some point in the past something like "async/await lets you write asynchronous code as if it were synchronous code"? Shame on us all for not picking up on this before, right?

Ok, so now you can truly program asynchronously. Happy coding!


Should I scare you a bit more? Let's.

If you are an avid .Net backend developer, you've probably coded hundreds of queries (or thousands) with Entity Framework. What does EF tell you about the data context methods? It tells you that you must always await them. Yes, one more reason for me to hate EF and love Dapper, and you should hate it too, but to each their own.

Let me know in the comments if you would like me to talk a bit about this and a few other performance hits you're taking by using EF.