Long Story Short: Async/Await Best Practices in .NET

Kabukichō (歌舞伎町), Tokyo — Ricoh GR

Async/Await — Introduction

Async/Await has been around since C# version 5.0 (2012) and has quickly become one of the pillars of modern .NET programming — any C# developer worth his/her salt should be using it to improve application performance, overall responsiveness, and code legibility.

Async/Await makes deceptively simple to implement asynchronous code, and removes the programmer from having to deal with the details of handling asynchronous code, but how many of us really know how it works underneath and what are the do’s and don’ts of this technique? There’s a lot of great information out there, but quite scattered, hence this article.

Let’s deep dive into it.

The first thing to know is that, behind the scenes, every time you have a method or function with Async/Await, the compiler actually turns your method into a generated class that implements the IAsyncStateMachine interface. This class is responsible for keeping the state of your method during the life cycle of the asynchronous operation — it encapsulates all the variables of your method as fields and splits your code into sections that are executed as the state machine transitions between states so that the thread can leave the method, and when it comes back, the state is intact.

As an example, here is a very simple definition of a class with two async methods:

A class with two async methods

If we examine the code generated upon build, we will see something like the below:

Note that we have 2 new internal classes generated for us, one for each async method. These classes hold the state machine for each of our async methods.

Further examining the decompiled code for <AsyncAwaitExample>d__0, we will notice that our internal variable “myVariable” is now a class level field:

We can also see other class-level fields used internally to maintain the state of the IAsyncStateMachine. The state machine transitions through states via the MoveNext() method, basically a big switch. Note how the continuation after each of the async calls occurs in different sections (preceded by a label).

What this means is that the elegance of async/await comes at a price. Using async/await actually adds a bit of complexity (that you may not be aware). In server-side logic, this may not be critical, but particularly when programming mobile apps, where every CPU cycle and kb of memory counts, you should keep this in mind, as the amount of overhead can quickly add up. We’ll discuss later on this article best practices to only use Async/Await where needed.

For a fairly didactical explanation of the state machine, check this youtube video by the On.NET show.

There are basically two scenarios where Async/Await is the right solution.

  • I/O-bound work: Your code will be waiting for something, such as data from a database, reading a file, a call to a web service. In this case you should use Async/Await, but not use the Task Parallel Library.
  • CPU-bound work: Your code will be performing a complex computation. In this case, you should use Async/Await but spawn the work off on another thread using Task.Run. You may also consider using the Task Parallel Library.

Async all the way

As you start working with async methods, you will quickly realize that the asynchronous nature of the code starts spreading up and down your hierarchy of method calls — meaning, you need to make your invoking code asynchronous as well and so on.

You may be tempted to “stop” this by blocking in your code using Task.Result or Task.Wait, converting just a small part of the application and wrapping it in a synchronous API so the rest of the application is isolated from the changes. Unfortunately, this is a recipe for creating hard to track deadlocks.

The best solution to this problem is to allow async code to grow naturally through the codebase. If you follow this solution, you’ll see async code expand to its entry point, usually an event handler or controller action. Embrace async all the way!

More information in this article by MSDN.

If a method is declared async, make sure there is an await!

As we already discussed, when the compiler finds an async method, it turns the method into a State Machine. If your code does not have an await in its body, the compiler will generate a warning but the state machine will be created nevertheless, adding unnecessary overhead for an operation that will actually never yield.

Avoid…async void

Async void is a big no-no. As a rule of thumb consider using async Task instead of async void.

An async void and an async Task method

There are several reasons for this, including:

  • Exceptions thrown in an async void method can’t be caught outside of that method: When an exception is thrown out of an async Task or async Task<T> method, that exception is captured and placed on the Task object. With async void methods, there is no Task object, so any exceptions thrown out of an async void method will be raised directly on the SynchronizationContext that was active when the async void method started.

Consider the example below. The catch block will never be reached.

Exceptions thrown in an async void method can’t be caught outside of that method

Compare to this code where instead of async void we have an async Task. In this case, the catch is actually reached.

The exception is captured and placed on the Task object
  • async void methods can cause bad side effects if the caller isn’t expecting them to be async: If your asynchronous method returns nothing, use async Task (without “<T>” for the Task) as the return type.
  • Async void methods are very difficult to test: Because of the differences in error handling and composing, it’s difficult to write unit tests that call async void methods. The MSTest asynchronous testing support only works for async methods returning Task or Task<T>.

The exception to this best practice is asynchronous event handlers. Even in this case, it is recommended that you minimize the code written in the handler itself — await an async Task method that contains the actual logic.

More information in this article by MSDN.

Consider using return Task instead of return await

As it was already discussed, every time you declare a method as async, the compiler to create a State Machine class that actually wraps your method logic. This adds a certain amount of overhead that may add up, particularly for mobile devices, where we have stricter resource constraints.

Sometimes the method does not need to be async, but return a Task<T> and let the other side handle it as appropriate. If the last sentence of your code is a return await you may actually consider refactoring it so that the return type of the method is Task<T> (instead of async T). With this, you are avoiding the generation of the state machine, thus making your code leaner. The only time we truly want to await is when we do something with the result of the async task in the continuation of the method.

Consider using return Task instead of return await

Note that if we don’t have return await, but return a Task<T> instead, the return happens right away, so, if the code is inside a try/catch block, the exception will not be caught. Similarly, if the code is inside a using block, it will dispose the object right away. See next best practice.

Do not wrap return Task inside try..catch{} or using{} block

Return Task can cause unexpected behavior used inside a try..catch block (an exception thrown by the async method will never be caught) or inside a using block because the task will be returned right away.

If you need to wrap your async code in a try..catch or using block, use return await instead.

Do not wrap return Task inside try..catch{} or using{} block

More information in this stack overflow thread.

Avoid using .Wait() or .Result — Use GetAwaiter().GetResult() instead

If you have to block waiting the completion of an Async Task, use GetAwaiter().GetResult(). Wait and Result will wrap any exceptions within an AggregateException, which complicates error handling — The advantage of GetAwaiter().GetResult() is that it returns a normal exception instead of an AggregateException.

If you have to block waiting the completion of an Async Task, use GetAwaiter().GetResult().

More information in this link.

If a method is async, add the Async suffix to its name

This is the convention used in .NET to more-easily differentiate synchronous and asynchronous methods (except event handlers or web controller methods, but those should not be explicitly called by your code anyway).

Async library methods should consider using Task.ConfigureAwait(false) to boost performance

.NET framework has the notion of “synchronization context”, which represents a way to “get back to where you were before” — Whenever a Task is awaited, it captures current synchronization context before awaiting.

Upon Task completion, the .Post() method of synchronization context is invoked to resume where it was before. This is useful to get back to UI thread or to resume back to same ASP.NET context, etc.

When writing library code though, you rarely need to go back to the context where you were before. When Task.ConfigureAwait(false) is used, the code no longer tries to resume where it was before, instead, if possible, the code completes in the thread that completed the task, thus avoiding a context switch. This slightly boosts performance and can help avoid deadlocks as well.

As a rule of thumb use ConfigureAwait(false) for server-side processes and library code.

This is particularly important when the library method is called a large number of times, to have better responsiveness.

As a rule of thumb use ConfigureAwait(false) for server-side processes in general. We don’t care which thread is used for the continuation, as opposed to applications where we need to come back to UI thread.

Now…In ASP.NET Core, Microsoft did away with the SynchronizationContext, so in theory, you don’t need this. But, if you are writing library code that could be potentially reused in other applications (i.e: UI App, Legacy ASP.NET, Xamarin Forms), it remains a best practice.

For a good explanation on this concept, see this video by Channel 9.

Reporting progress from async Tasks

A fairly common use-case for async methods is to do work in the background, freeing up the UI thread to do other things, and maintain responsiveness. In this scenario, you may want to report progress back to the UI, so the user can follow the progress and interact with the operation.

In order to solve this common problem, .NET provides the IProgress<T> interface, which exposes a Report<T> method, which the async task invokes to report progress back to the caller. This interface is received as a parameter of the async method — the caller must provide an object that implements this interface.

.NET provides Progress<T>, a default implementation of IProgress<T>, which is actually encouraged to be used, as it handles all the low-level logic related to saving and restoring the synchronization context. Progress<T> also exposes an event and an Action<T> callback — both are called when the task reports progress.

Together, IProgress<T> and Progress<T> provide an easy way to pass progress information from a background task to the UI thread.

Note that <T> can be a simple value such as an int, or an object that provides contextual information about the progress, such as progress percentage, a string description of the current operation, ETA, and so on.

Do consider how often you report progress. Depending on the operation at hand, you may find that your code reports progress several times per second, which may actually cause the UI to become less responsive. In this kind of scenario, it is recommended to report progress back in coarser-grained intervals.

More information in this article by official Microsoft .NET Blog.

Cancelling async Tasks

Another common use-case for background tasks, is having the ability to cancel execution. .NET provides the CancellationToken class. The async method receives a CancellationToken, which is then shared by the caller code, and the async method, thus providing a mechanism to signal cancellation.

In the most common case, cancellation follows this flow:

  1. The caller creates a CancellationTokenSource object.
  2. The caller calls a cancelable async API, and passes the CancellationToken from the CancellationTokenSource (CancellationTokenSource.Token).
  3. The caller requests cancellation using the CancellationTokenSource object (CancellationTokenSource.Cancel()).
  4. The task acknowledges the cancellation and cancels itself, typically using the CancellationToken.ThrowIfCancellationRequested method.

Note that in order for this mechanism to work, you will need to write code to check for the cancellation being requested at regular intervals (i.e: on every iteration of your code, or at natural stopping point in the logic). Ideally, once cancellation has been requested, an async Task should cancel as quickly as possible.

You should consider using cancellation for all methods that may take a long time to complete.

More information in this article by official Microsoft .NET Blog.

Reporting progress and cancellation — An example

Waiting a period of time

If you need to wait for a period of time (for example, to retry to check if a resource becomes available), make sure to use Task.Delay — never use Thread.Sleep in this scenario.

Awaiting the completion of multiple async Tasks

Use Task.WaitAny to wait for any task to complete. Use Task.WaitAll to wait for all tasks to complete.

A melomaniac that happens to be in the trenches of software development

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store