Async and Await in .NET 8 — Common Mistakes and Best Practices

mr. sci. Admir Mujkic
5 min readDec 9, 2023

In the realm of modern .NET development, the async/await paradigm stands as a cornerstone for efficient asynchronous programming.

This scholarly exploration, inspired by the profound insights of Brandon Minnick at the Copenhagen DevFest 2023, delves into the nuanced best practices and common pitfalls associated with async/await in .NET 8.

Each concept is elucidated with a C# code example and a detailed explanation, aiming to provide a comprehensive understanding for both novice and seasoned developers.

Async and Await in .NET 8 — Common Mistakes and Best Practices

In-Depth Best Practices with Examples

The Imperative of Non-Blocking Calls

In asynchronous programming, avoiding blocking calls is paramount to prevent deadlocks and ensure the responsiveness of applications.

// Antipattern
var result = myAsyncMethod().Result;

// Recommended Approach
var result = await myAsyncMethod();

Utilizing .Result synchronously blocks the calling thread until the task's completion, potentially leading to deadlocks. In contrast, await asynchronously waits for task completion, thus maintaining application responsiveness and fluidity.

Optimization with ConfigureAwait

The judicious use of ConfigureAwait(false) in non-UI contexts can significantly enhance performance by reducing unnecessary context switching.

var data = await myAsyncMethod().ConfigureAwait(false);

The ConfigureAwait(false) directive instructs the runtime to forgo returning to the original synchronization context (such as the UI thread), thereby reducing overhead and enhancing performance in non-UI scenarios.

Robust Exception Handling in Async/Await

Exception handling is a critical aspect of async/await patterns, ensuring that errors are properly managed and debugged.

try
{
await myAsyncMethod();
}
catch (Exception ex)
{
// Exception handling logic
}

Employing await within a try-catch block ensures that any exceptions thrown during the task's execution are appropriately caught and handled, thereby preventing unobserved exceptions and enhancing application stability.

Cancellation Tokens and Enhancing Responsiveness

Implementing cancellation tokens in asynchronous methods is a best practice that provides a mechanism to cancel long-running or unnecessary operations, thus enhancing the dynamism of the application.

var cancellationToken = new CancellationTokenSource();
var task = myAsyncMethod(cancellationToken.Token);
cancellationToken.Cancel();

Cancellation tokens offer a responsive way to cancel ongoing async operations, providing greater control over the application’s flow and resource management.

Efficient Streaming with Async Enumerable

Efficient handling of streaming data in asynchronous programming is optimally achieved using IAsyncEnumerable.

await foreach (var item in myAsyncStreamMethod())
{
// Immediate processing of each streamed item
}

IAsyncEnumerable and await foreach facilitate the efficient processing of streamed data, allowing each item to be processed as soon as it becomes available, thus enhancing the efficiency of data handling in asynchronous operations.

ValueTask — A Tool for Quick Operations

The choice between ValueTask and Task can have performance implications, particularly in scenarios where methods frequently complete synchronously.

public async ValueTask<int> GetFastResultAsync()
{
return 42; // Fast, synchronous path
}

ValueTask, being a struct, offers a more efficient alternative to Task in scenarios where methods often complete synchronously, thereby reducing the overhead associated with task management.

The Risks of Async Void

Understanding the implications of async void is essential for error handling and maintaining the stability of applications.

public async Task PerformOperationAsync()
{
// Asynchronous operation
}

The use of async Task over async void is recommended for better error handling and predictability. async void methods can lead to unhandled exceptions and should be reserved for specific use cases like event handlers.

Deferred Execution of Tasks

Optimizing task execution by deferring the creation of the async state machine can lead to more efficient and performant code.

public Task<MyResult> GetResultAsync()
{
return anotherAsyncMethod();
}

Directly returning a task from a method avoids the unnecessary overhead of an async state machine when no additional await operations are required within the method, thereby optimizing performance.

Proper Management with Async Disposable

Proper management of resources in asynchronous operations is crucial to prevent resource leaks and ensure efficient resource utilization.

await using (var resource = new AsyncResource())
{
// Resource utilization
}

The IAsyncDisposable interface and the await using construct ensure the proper asynchronous disposal of resources, thus preventing resource leaks and enhancing application performance.

Common Pitfalls with Examples

Blocking the Main Thread: A Cautionary Tale

Blocking the main thread in asynchronous programming can lead to a poor user experience and application freezes, a situation to be diligently avoided.

var result = myAsyncMethod().Result; // Blocks the main thread

Utilizing .Result in an asynchronous method can freeze the UI by blocking the main thread. The await keyword should be the default choice for asynchronous operations to maintain UI responsiveness.

The Perils of Ignoring Exceptions

Properly handling exceptions in async/await patterns is crucial for robust and stable applications. Failing to await a task can lead to exceptions being unobserved, complicating the debugging process and potentially leading to unstable application behavior.

Misusing Async Void — A Hazardous Practice

The misuse of async void can lead to unhandled exceptions and unpredictable application behavior, undermining the stability and reliability of the application.

public async void UnstableAsyncOperation()
{
// Risky async operation
}

async void methods can cause unhandled exceptions and unpredictable behavior, making them a risky choice for any operation outside of specific event handler scenarios.

The Consequences of Neglecting ConfigureAwait

Overlooking the ConfigureAwait directive in certain contexts can lead to inefficient task scheduling and performance issues, a situation that demands careful consideration.

await myAsyncMethod(); // In a non-UI context without ConfigureAwait(false)

In non-UI code, neglecting ConfigureAwait(false) can lead to inefficient context switching, impacting the overall performance and efficiency of the task execution.

The Impact of Ignoring Cancellation

Failing to implement cancellation in async operations can render applications less responsive and more resource-intensive, highlighting the importance of responsive design.

await LongRunningOperationWithoutCancellation(); // No way to cancel

Not implementing cancellation tokens can make your application less responsive and more resource-intensive, underscoring the importance of incorporating cancellation mechanisms in long-running operations.

The Dangers of Overusing Task.Run

The overuse of Task.Run for unnecessary offloading can lead to thread pool starvation and degraded performance, a scenario that warrants judicious use of resources.

await Task.Run(() => SynchronousMethod()); // Unnecessary offloading

Unnecessary use of Task.Run can lead to thread pool starvation and degraded performance, emphasizing the need for careful consideration before offloading tasks to the thread pool.

Inefficient Use of Async Enumerable — A Missed Opportunity

Efficiently processing streamed data is key in asynchronous programming, and misusing IAsyncEnumerable can lead to inefficiencies, highlighting the need for proper utilization of this feature.

Not using IAsyncEnumerable for streaming data can lead to inefficient processing, underscoring the importance of leveraging this feature for optimal data handling in asynchronous operations.

Improper Use of Async Disposable

Not using await using for asynchronous disposal can lead to resource leaks and performance issues, highlighting the importance of proper resource management in asynchronous programming.

using (var resource = new AsyncResource()) // Should be await using
{
// Potential resource leak
}

Failing to use await using for asynchronous disposal can result in resource leaks and performance degradation, underscoring the necessity of proper resource management in async/await patterns.

Conclusion

The async/await paradigm in .NET 8 is a powerful tool that requires a nuanced understanding to fully harness its capabilities. By adhering to these best practices and avoiding common pitfalls, developers can ensure robust, efficient, and optimized asynchronous code. This exploration aims to provide a deep understanding of async/await, empowering developers to write code that is not only effective but also adheres to the highest standards of modern .NET development.

Cheers👋

--

--

mr. sci. Admir Mujkic

Admir combined engineering expertise with business acumen to make a positive impact & share knowledge. Dedicated to educating the next generation of leaders.