Avoid async void

Avoid async void

Introduction

Asynchronous programming is a crucial aspect of modern software development, allowing applications to perform non-blocking operations and maintain responsiveness. In C#, asynchronous programming is accomplished using the async and await keywords.

While these tools make it easier to work with asynchronous code, there are important best practices to follow to ensure your code is maintainable, testable, and reliable. In this blog post, we will explore these best practices, including the avoidance of async void and the use of async Task or async ValueTask instead.

The Pitfalls of async void

One common mistake in asynchronous programming is using async void methods. While they may seem convenient, async void methods can lead to issues with error handling, testing, and maintainability.

Here’s why it’s best to avoid them:

1. Lack of Control Over Asynchronous Operations

In an async void method, you cannot use the await keyword to asynchronously wait for a task to complete. This means you lose control over the completion of the operation, making it challenging to coordinate multiple asynchronous operations.

2. Unhandled Exceptions

Exceptions thrown within an async void method cannot be caught using a try…catch block within that method. Unhandled exceptions can crash the application or result in unpredictable behavior, making error handling difficult.

3. Limited Testing Scenarios

Unit testing async void methods can be challenging because there’s no straightforward way to wait for the asynchronous operation to complete and verify results. This can hinder testability and make it harder to write robust unit tests.

4. Debugging Challenges

Debugging async void methods can be complex because you don’t have the same level of control over error handling and breakpoints. Diagnosing issues when exceptions are thrown in async void methods can be challenging.

What to Do Instead: async Task and async ValueTask

To avoid the pitfalls of async void, use async Task or async ValueTask for your asynchronous methods. Here’s how to follow this best practice:

1. Use async Task for Methods

Define your asynchronous methods with the return type of Task when you want to perform asynchronous operations and have control over their completion.

public async Task SomeAsyncMethod()
{
    // Asynchronous operations
    await DoSomethingAsync();
}

2. Use async ValueTask for High-Performance Methods

For methods that perform asynchronous operations and could benefit from the potential performance improvements offered by ValueTask, return ValueTask instead of Task.

public async ValueTask SomeAsyncMethod()
{
    // Asynchronous operations
    await DoSomethingAsync();
}

3. Handle Exceptions Gracefully

Ensure that you handle exceptions within your async Task or async ValueTask methods to prevent unhandled exceptions from crashing your application. Use try…catch blocks to handle exceptions and take appropriate actions.

public async Task SomeAsyncMethod()
{
    try
    {
        // Asynchronous operations
        await DoSomethingAsync();
    }
    catch (Exception ex)
    {
        // Handle exceptions
    }
}

4. Await Asynchronous Operations

Use the await keyword to asynchronously wait for the completion of other asynchronous operations within your async Task or async ValueTask methods.

public async Task SomeAsyncMethod()
{
    // Asynchronous operations
    await DoSomethingAsync();
}

5. Return Task for Unit Testing

Returning Task or ValueTask allows you to write unit tests for your asynchronous methods more easily. You can await these methods in your test code to ensure that asynchronous operations complete as expected.

[TestMethod]
public async Task TestSomeAsyncMethod()
{
    // Arrange

    // Act
    await SomeAsyncMethod();

    // Assert
    // Add your assertions here
}

Conclusion

By following best practices for asynchronous programming in C#, such as avoiding async void and using async Task or async ValueTask instead, you can build more maintainable, testable, and reliable asynchronous code.

These practices provide better control over the flow of asynchronous operations and facilitate effective error handling, making your code more robust and predictable.

You can find the official async documentation here: – https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/async-scenarios.

I have written a few other posts on similar subjects here: – Understanding Task.Yield, Asynchronous Task.Delay, Asynchronous Testing with NUnit and C#.

Stephen

Hi, my name is Stephen Finchett. I have been a software engineer for over 30 years and worked on complex, business critical, multi-user systems for all of my career. For the last 15 years, I have been concentrating on web based solutions using the Microsoft Stack including ASP.Net, C#, TypeScript, SQL Server and running everything at scale within Kubernetes.