Skip to main content

Command Palette

Search for a command to run...

Mistakes and best practices with async & await patterns in C#

Updated
3 min read
Mistakes and best practices with async & await patterns in C#
P
Senior Software Engineer specialising in cloud architecture, distributed systems, and modern .NET development, with over two decades of experience designing and delivering enterprise platforms in financial, insurance, and high-scale commercial environments. My focus is on building systems that are reliable, scalable, and maintainable over the long term. I’ve led modernisation initiatives moving legacy platforms to cloud-native Azure architectures, designed high-throughput streaming solutions to eliminate performance bottlenecks, and implemented secure microservices environments using container-based deployment models and event-driven integration patterns. From an architecture perspective, I have strong practical experience applying approaches such as Vertical Slice Architecture, Domain-Driven Design, Clean Architecture, and Hexagonal Architecture. I’m particularly interested in modular system design that balances delivery speed with long-term sustainability, and I enjoy solving complex problems involving distributed workflows, performance optimisation, and system reliability. I enjoy mentoring engineers, contributing to architectural decisions, and helping teams simplify complex systems into clear, maintainable designs. I’m always open to connecting with other engineers, architects, and technology leaders working on modern cloud and distributed system challenges.

Asynchronous programming is an essential component of modern software development, particularly in C# and the .NET framework. Its ability to greatly enhance application responsiveness, scalability, and resource efficiency has made it indispensable in today's increasingly parallel and network-bound application environments. For instance, web APIs benefit significantly from asynchronous calls by efficiently managing multiple simultaneous requests, thus improving overall throughput and response times. One of the most frequent and critical errors in asynchronous programming is using async void methods. Although convenient, especially in event handlers such as button clicks (async void Button_Click), this pattern lacks proper exception handling capabilities. If an exception occurs within an async void method, it often leads to unhandled exceptions, potentially causing the application to terminate unexpectedly or behave unpredictably. For example, consider this problematic snippet:

async void ProcessData()
{
    await Task.Delay(1000);
    throw new Exception("Error in async void method");
}

To handle exceptions correctly and ensure predictable behavior, developers should consistently use async Task or async Task<T> instead:

async Task ProcessDataAsync()
{
    await Task.Delay(1000);
    throw new Exception("Error in async Task method");
}

// Proper exception handling
try
{
    await ProcessDataAsync();
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}

Another prevalent mistake is forgetting to correctly await asynchronous methods. Omitting the await keyword can inadvertently cause asynchronous operations to run concurrently, resulting in race conditions or unpredictable execution order. For example:

// Incorrect approach
var task1 = Task.Delay(5000);
var task2 = Task.Delay(3000);
// Tasks run concurrently unintentionally

// Correct approach
await Task.Delay(5000);
await Task.Delay(3000); // Tasks run sequentially as intended

Properly awaiting tasks ensures operations complete in the intended sequence, preserving logical flow and predictability of your code.

Mixing synchronous and asynchronous code incorrectly is another critical issue, particularly when using .Result or .Wait() in asynchronous contexts. Such synchronous calls within asynchronous methods can lead to thread-pool starvation or deadlocks, especially in UI or web service environments. For example:

// Problematic code causing potential deadlocks
public async Task FetchDataAsync()
{
    var result = GetDataAsync().Result; // Can cause deadlock
}

// Recommended approach
public async Task FetchDataAsync()
{
    var result = await GetDataAsync(); // Avoids deadlock
}

Maintaining asynchronous methods end to end throughout your application's stack by consistently using await helps avoid such blocking issues entirely.

Implementing cancellation tokens effectively is another essential practice in asynchronous programming. Cancellation tokens enable developers to gracefully handle task cancellations, improving responsiveness and resource management. Properly implemented, cancellation tokens help avoid resource leaks and ensure consistent application state handling, especially in operations that could run indefinitely. Here's a simple example demonstrating cancellation:

public async Task LongRunningOperationAsync(CancellationToken cancellationToken)
{
    for (int i = 0; i < 100; i++)
    {
        cancellationToken.ThrowIfCancellationRequested();
        await Task.Delay(1000, cancellationToken);
    }
}

// Usage
var cts = new CancellationTokenSource();
await LongRunningOperationAsync(cts.Token);
// cts.Cancel(); // Can be called to cancel operation

Finally, careful exception handling within asynchronous methods is crucial. Exceptions should be caught within tasks and appropriately propagated to calling methods, allowing applications to recover or provide meaningful user feedback. Properly structured Try/Catch blocks within asynchronous code ensure that errors are managed predictably:

public async Task DoWorkAsync()
{
    try
    {
        await Task.Run(() => throw new Exception("Task failure"));
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Caught exception: {ex.Message}");
        throw; // Re-throw if further handling is required upstream
    }
}

Understanding these common mistakes and sticking to recommended best practices in asynchronous programming makes us write clearer, safer, and more maintainable code. Consequently, adopting these practices significantly enhances application stability, responsiveness, and user experience, ensuring modern applications efficiently meet user expectations and business requirements.