A Guide to C# async/await Best Practices
A summary of the most important best practices for writing asynchronous code in C# with async and await. Learn how to avoid common pitfalls like deadlocks and how to write clean, efficient async code.
The async
and await
keywords in C# have made asynchronous programming dramatically simpler. However, writing correct and efficient async code requires understanding a few key best practices. Following these guidelines will help you avoid common pitfalls and get the most out of the async/await pattern.
1. Async All the Way
This is the most important rule. Once you start writing async
code, you should prefer to let it flow through your entire call stack. If you call an async
method, you should await
it, which means your calling method must also be async
.
Don't block on async code. Avoid using methods like Task.Wait()
or Task.Result
. This is known as "sync over async" and it can lead to serious problems, most notably deadlocks, especially in UI and ASP.NET Classic applications.
Bad Practice:
public void MyMethod()
{
// This can cause deadlocks!
var result = MyAsyncMethod().Result;
}
Good Practice:
public async Task MyMethodAsync()
{
var result = await MyAsyncMethod();
}
2. Use ConfigureAwait(false)
in Libraries
When you await
a task, the default behavior is to capture the current synchronization context and resume the execution on that same context after the await completes. In a UI application, this is the UI context. In an ASP.NET (pre-Core) application, this is the request context.
This is useful for application-level code, but it's unnecessary and potentially harmful for general-purpose library code. In a library, you should use ConfigureAwait(false)
to tell the runtime that it can resume the execution on any available thread pool thread.
public async Task<string> GetDataAsync()
{
var client = new HttpClient();
// Don't need to resume on the original context
var content = await client.GetStringAsync("http://example.com").ConfigureAwait(false);
return content;
}
Using ConfigureAwait(false)
improves performance and helps prevent deadlocks. This is a best practice for any non-application-level code.
3. Return Task
or Task<T>
, Not void
An async
method can have three possible return types:
Task
: For an async method that does not return a value.Task<T>
: For an async method that returns a value of typeT
.void
: Avoid this!
An async void
method is a "fire and forget" method. The caller has no way to await
it, and more importantly, any exceptions thrown from an async void
method cannot be caught in a standard try-catch
block and will typically crash the process.
The only valid use case for async void
is for event handlers (e.g., a button click event in a UI application), which are required by the event system to have a void
return type.
4. Prefer ValueTask<T>
for Performance-Critical, Synchronous Scenarios
For most application code, Task<T>
is the correct choice. However, if you are writing a high-performance library where an async
method is expected to complete synchronously very often (e.g., by returning a value from a cache), you can use ValueTask<T>
to avoid allocating a Task
object on the heap. This is an advanced optimization and should only be used when you've identified a performance bottleneck.
5. Use CancellationToken
for Cancellable Operations
Many asynchronous operations can take a long time. It's good practice to make them cancellable. You can do this by accepting a CancellationToken
as a parameter to your async
method and passing it down to the other async methods you call.
public async Task DoWorkAsync(CancellationToken cancellationToken)
{
// Pass the token to other async calls
await Task.Delay(1000, cancellationToken);
// Periodically check if cancellation has been requested
cancellationToken.ThrowIfCancellationRequested();
}
This allows the caller to cancel the operation if it's no longer needed.
Conclusion
async
and await
are incredibly powerful features for writing responsive and scalable .NET applications. By following these best practices—especially "async all the way" and using ConfigureAwait(false)
in libraries—you can avoid common pitfalls and write asynchronous code that is clean, correct, and efficient.