Skip to main content

Async/await tips and tricks

Many .NET developers have been busy upgrading their code to take advantage of the async and await keywords. These keywords make asynchronous programming a lot easier by allowing us to represent a call to an asynchronous method almost as if it were a synchronous one. We just add the await keyword, and the compiler does the hard work of dividing the method into sections and keeping track of where to resume execution once async work completes.

However, it’s difficult to hide all the complexity of asynchronous programming behind a couple keywords, and there are a host of pitfalls and gotchas that you should be aware of. Without proper tooling, it’s all too easy for any one of them to sneak up and bite you.

We’ve experienced this firsthand. Through the development of NServiceBus 6.0, we’ve learned quite a bit about async/await. In the latest version, NServiceBus was completely upgraded to support async/await from top to bottom, for every API that could potentially be I/O bound. There’s so much async in NServiceBus that we’re intentionally not using the -Async suffix (for good reason) in any of our APIs.

In this article, we’ll share some of the tools and techniques we’ve found that can help make your transition to async/await as smooth as possible.

🔗Treat warnings as errors

When you’re writing async code, it can be really easy to forget to await a Task-returning method. You might accidentally or absentmindedly write code like this:

public async Task Handle(DoSomething message, IMessageHandlerContext context)
{
    context.Publish(new SomethingHappened { Id = message.Id });
    // Oops, forgot to await!
}

When you do this (and you will eventually), the IDE will quietly remind you with a blue squiggly line and compiler warning CS4014: “Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the ‘await’ operator to the result of the call.”

The await keyword is what brings the result of the asynchronous operation back into the calling method, both in cases of success and failure. That means that if you forget the await keyword, an exception raised within that method may be silently ignored. Now, we all know to avoid creating empty catch blocks, but that’s exactly what’s caused by forgetting the await keyword.

The thing is that it’s just too easy to miss these warnings. To make sure these aren’t forgotten, consider enabling the Treat warnings as errors option in your build configuration to ensure that these useful hints won’t be missed.

🔗ConfigureAwait

Microsoft’s async/await best practices state that you should use ConfigureAwait(false) almost anytime you await something. This way, the TaskScheduler knows that it can use any available thread when the method resumes after the await. The only exception to the rule is when execution must continue on a specific thread, such as the UI thread in a client application.

When you use ConfigureAwait in your code, it often looks something like this, with the .ConfigureAwait(false) call tacked on to the end of the method that returns a Task:

public async Task AlreadyAsyncMethod()
{
    await DoSomethingAsync().ConfigureAwait(false);
}

The reason ConfigureAwait is used here is to enable tasks to continue on any available thread. For more information on why this is important, check out the article Context Matters. Suffice it to say, for library code that doesn’t care which thread it executes on, you should use ConfigureAwait(false) every time you use await. Eventually, you will find that adding ConfigureAwait all over the place becomes tiresome and is easy to forget.

There are two ways you can deal with this. You can use a Roslyn-based tool to make sure you never forget to add ConfigureAwait, or you can use IL weaving so you don’t have to type it manually at all. Either way, you’ll come out ahead.

🔗Never forget

The best way to ensure you never forget about ConfigureAwait is to have the compiler remind you. The .NET Compiler Platform (“Roslyn”) is the perfect tool for this.

We created the Particular.CodeRules NuGet package to enforce our own usage of ConfigureAwait using Roslyn. With this package in place as a project dependency, forgetting to use ConfigureAwait will result in a compile-time error that fails the build — and nobody will ignore that.

If you like, you can use the NuGet package directly or check out the source code on GitHub.

🔗Set it and forget it

All those calls to ConfigureAwait(false) can get pretty distracting when littered throughout your code. If you’d rather not deal with them at all, there’s a way to include them with tooling instead.

You can do this with the ConfigureAwait add-in for Fody, which allows you to set your ConfigureAwait preference at a global level. You can add an attribute to an individual method, a class, or even an entire assembly, and then Fody will add the .ConfigureAwait(value) to the assembly’s intermediate language (IL) during compilation at any point where the await keyword is used.

With that preference set, the tooling takes care of the rest, and you can forget about typing .ConfigureAwait(false) ever again.

🔗Optimize async usage

Anytime you’re implementing a method that returns a Task or a Task<T>, you have the option of dealing exclusively in tasks or adding the async and await keywords and letting the compiler generate a state machine to run the async continuations for you.

And so, these two methods are functionally equivalent. The only difference is the decision to return the Task or await it:

// Return the Task from the Publish operation directly
public Task Handle(DoSomething message, IMessageHandlerContext context)
{
    return context.Publish(new SomethingHappened { Id = message.Id });
}

// Add the async keyword and await the Publish
public async Task Handle(DoSomething message, IMessageHandlerContext context)
{
    await context.Publish(new SomethingHappened { Id = message.Id });
}

The thing is, the state machine created by the second example is not entirely free. Every bit of executed code, whether created by you or by the compiler, has a cost.

When a method only awaits one operation and does so at the end of the method, it’s usually not worth it. Instead, we can directly return the task, as shown in the first method, and avoid the cost of the async state machine altogether. In addition, we don’t need ConfigureAwait(false) when the Task isn’t awaited either.

These are only small differences in performance, and in the large majority of business code out there, it’s probably premature optimization and won’t really matter. However, if you are writing code on the hot path in a performance-sensitive system, these things can add up.

Luckily, there is a tool that can easily point out these cases for you. It’s called AsyncFixer and is available either as a Visual Studio extension or a NuGet package. In addition to pointing out unnecessary usages of async/await (and offering to fix them for you), the extension will save you from breaking other async/await best practices such as using async void methods.

🔗Summary

Writing asynchronous code doesn’t have to require a state of constant vigilance to make sure we avoid pitfalls. Smart developers use tooling to make writing asynchronous code as simple and smooth as possible.

These tools have certainly been helpful to us as we’ve been implementing async in NServiceBus Version 6. We hope that you find them useful when designing your own systems as well.

And, finally, if you’re interested in more guidance on async/await, check out our async/await webinar series to see how you can avoid some of the more common problems in asynchronous codebases.

Do you have any other tools you find useful for wrangling async code? If so, let us know in the comments. We’d love to hear from you.


About the author: David Boike is a developer at Particular Software who used to forget to use ConfigureAwait at least once per week until he found the tooling to handle that for him.

Share on Twitter
Don't miss a thing. Sign up today and we'll send you an email when new posts come out.
Thank you for subscribing. We'll be in touch soon.
 
We collect and use this information in accordance with our privacy policy.