async await in C#: 3 Beginner Tips You Need to Know

Async await in C# is a feature that allows you to write asynchronous code that is easy to read and maintain. It greatly simplifies how you handle asynchronous operations by providing a structured way to await tasks and their results. With the rate at which computers are scaling with respect to performance & cores, and our growing need to interface with many external services, async await in C# can make our developer lives easier.

In the following sections, I'll highlight 3 beginner tips for using async await in C#. Each tip will be accompanied by a code example, allowing you to see these concepts in action. These tips will cover important aspects such as error handling, cancellation, and performance optimization, so you'll be off to the races writing async code.


What's In This Article: async await in C#

Remember to check out these platforms:

// FIXME: social media icons coming back soon!


Tip 1: Understand the Basics of async await in C#

Async await in C# are two special keywords that are used to handle asynchronous code execution. Asynchronous programming enables us to write C# code that can perform multiple operations concurrently, improving the overall performance and responsiveness of an application. In C#, a task represents an asynchronous operation and it's used to encapsulate a unit of work that can be executed concurrently with other tasks. When using async/await, you can use tasks to handle and manage asynchronous operations in a structured way.

To define an async method in C#, you need to add the async modifier to the method signature. For example:

public async Task DoAsyncWork()
{
    // Asynchronous code here
}

In the above example, the method DoAsyncWork is declared as async and returns a Task. This indicates that the method will perform asynchronous operations and can be awaited.

Within the async method, you can use the await keyword to signal the point where the execution should wait for the completion of an asynchronous operation. For example:

public async Task DoAsyncWork()
{
    // Perform asynchronous operations
    await Task.Delay(1000);

    // Continue with other operations
}

In the above example, the await Task.Delay(1000) statement pauses the execution of the method until the Task.Delay operation completes. This allows other code, such as other async tasks, to execute concurrently. By using tasks, you can harness the power of asynchronous programming in C#.


Tip 2: Handling Multiple Exceptions with async await

Exception handling is an important aspect of writing any software application. When working with async code in C#, handling exceptions can present some challenges due to the asynchronous nature of the code. In certain situations, multiple exceptions can occur when executing async code. It's important to be able to handle and aggregate these exceptions effectively. To handle multiple exceptions, C# provides the AggregateException class, which allows us to group and manage multiple exceptions as a single entity.

When multiple exceptions occur in an async operation, the AggregateException class collects these exceptions and allows us to access and handle them in a centralized manner. This can be particularly useful for scenarios where we need to process all exceptions generated during an async operation. To handle exceptions using the AggregateException class, we can wrap the code that may throw exceptions in a try-catch block. Within the catch block, we can access the InnerExceptions property of the AggregateException to access each individual exception that occurred.

Here's an example of how to use the AggregateException class to handle and aggregate exceptions in async code:

try
{
    await SomeAsyncOperation();
}
catch (AggregateException ex)
{
    foreach (var innerException in ex.InnerExceptions)
    {
        // Handle each individual exception
        Console.WriteLine($"Exception: {innerException.Message");
    }
}

In the example above, SomeAsyncOperation represents an async operation that may throw one or more exceptions. By wrapping the await statement in a try-catch block and catching an AggregateException, we are able to access and handle each individual exception that occurred within the async operation. By properly handling and aggregating exceptions in async code using the AggregateException class, we can ensure that our code can gracefully handle any unexpected errors that may occur during async operations.


Tip 3: Avoiding Deadlocks with async await

Deadlocks can occur when two or more tasks are waiting for each other to complete indefinitely, resulting in a state where none of the tasks can proceed.

Stop Blocking Your Async Calls!

One of the most common causes of deadlocks in async methods includes using the Task.Result or Task.Wait methods inside an async method, which blocks the main thread and can lead to deadlocks. The best advice for avoiding deadlocks with async code:

“Don’t block on async code”

Stephen Cleary

He points out in his article that this tip (stop blocking) or the tip that immediately follows this (configuring the awaiter) are both able to help prevent deadlocks... but relying ONLY on the next pattern is more of a risk. That is, of these two suggestions, making your C# code async await all of the way through is the safer bet.

That means instead of having something like:

public class MyController(
  IWeatherService _weatherService) :
  ApiController
{
  public string GetWeatherData()
  {
    var weatherData = _weatherService.GetWeatherData(...).Result;
    return Results.OK(weatherData);
  }
}

Instead, we'd make the top-level async as well:

public class MyController(
  IWeatherService _weatherService) :
  ApiController
{
  public async Task<string> GetWeatherData()
  {
    var weatherData = await _weatherService.GetWeatherData(...);
    return Results.OK(weatherData);
  }
}

As soon as we introduce a .Result or .Wait() call, we are effectively blocking -- and that is one of the key situations that can lead to deadlocks as the application gets more complex.

Using ConfigureAwait(false) with Async Await in C#

In order to prevent deadlocks, the ConfigureAwait method can be used with the false parameter. This instructs the await operator to not capture the current synchronization context, allowing the continuation to be executed on any available thread from the thread pool -- not just the thread that started the call!

With ConfigureAwait(false), we can explicitly state that we do not require the original synchronization context to be captured, preventing the deadlock issue. Keep in mind that using ConfigureAwait(false) should be done carefully and only when it is absolutely necessary, as it may introduce potential issues if the code relies on the captured synchronization context for specific requirements. For example, if you're running UI logic and you come back to execution not on the UI thread, you may be in for some surprises!

Let's take a look at an example to illustrate the usage of ConfigureAwait(false) to prevent deadlocks:

public async Task<string> GetDataAsync()
{
    // NOTE: assume we enter this method on some given thread

    // use a false parameter on ConfigureAwait
    await SomeAsyncMethod().ConfigureAwait(false);

    // NOTE: we could be on a different thread than when we started this method!

    return ProcessData();
}

In the above example, SomeAsyncMethod is awaited using ConfigureAwait(false). This allows the continuation of the code to execute on any available thread, ensuring that the synchronization context is not captured and preventing potential deadlocks.

And while Stephen Cleary does say that this will help prevent deadlocks, he also had this to add:

Using ConfigureAwait(false) to avoid deadlocks is a dangerous practice. You would have to use ConfigureAwait(false) for every await in the transitive closure of all methods called by the blocking code, including all third- and second-party code. Using ConfigureAwait(false) to avoid deadlock is at best just a hack).

Stephen Cleary

Wrapping up Async Await in C#

In this article, I've discussed 3 beginner tips for using async await in C# to write concurrent code. Async await in C# can be a lot to absorb right in the beginning, so it will take some time and practice for it to feel more natural!

We got to see the basics of the keyword usage for async await -- With this, you can now write asynchronous code! From there, we looked at how multiple exceptions can arise in async await code paths and how to navigate that. And we concluded with some ideas around preventing deadlocks by looking at 2 possibilities we can leverage.

Async await in C# is something you're going to want to learn and practice! If you found this useful and you're looking for more learning opportunities, consider subscribing to my free weekly software engineering newsletter and check out my free videos on YouTube!