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
Stephen ClearyConfigureAwait(false)
to avoid deadlocks is a dangerous practice. You would have to useConfigureAwait(false)
for everyawait
in the transitive closure of all methods called by the blocking code, including all third- and second-party code. UsingConfigureAwait(false)
to avoid deadlock is at best just a hack).
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!
Affiliations
These are products & services that I trust, use, and love. I get a kickback if you decide to use my links. There’s no pressure, but I only promote things that I like to use!
- BrandGhost: My social media content and scheduling tool that I use for ALL of my content!
- RackNerd: Cheap VPS hosting options that I love for low-resource usage!
- Contabo: Affordable VPS hosting options!
- ConvertKit: The platform I use for my newsletter!
- SparkLoop: Helps add value to my newsletter!
- Opus Clip: Tool for creating short-form videos!
- Newegg: For all sorts of computer components!
- Bulk Supplements: Huge selection of health supplements!
- Quora: I answer questions when folks request them!