C# Async Programming Best Practices
Apply these practices when writing, reviewing, or refactoring asynchronous C# code.
Naming Conventions
- Suffix all async methods with
Async (e.g., GetDataAsync for a synchronous GetData)
- Match names with synchronous counterparts when both exist
Return Types
- Return
Task<T> when the method produces a value
- Return
Task when the method has no return value
- Use
ValueTask<T> when the method frequently completes synchronously (e.g., cache hits, short-circuit returns) — this avoids a Task heap allocation on the hot path
- Never return
async void except in event handlers — async void methods swallow exceptions and cannot be awaited, making errors invisible to the caller
CancellationToken Propagation
CancellationTokens are the cooperative cancellation mechanism in .NET. Dropping a token silently means the user loses the ability to cancel an operation that might be long-running or expensive.
- Accept
CancellationToken as the last parameter in every async method signature
- Always forward the token through the entire call chain — never silently drop it
- Pass tokens to every framework method that accepts one (
HttpClient, Stream, DbCommand, Channel, EF Core queries, etc.)
- Use
token.ThrowIfCancellationRequested() inside long-running CPU loops to make them responsive to cancellation
- Catch
OperationCanceledException at boundary layers (API controllers, hosted service entry points) — do not swallow it or rethrow as a different exception type
- Use
CancellationTokenSource.CreateLinkedTokenSource() to combine scopes — for instance, merging a request-scoped token with a user-initiated cancel:
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(requestToken, userCancelToken);
await ProcessAsync(linkedCts.Token);
Exception Handling
- Wrap
await expressions in try/catch to handle faulted tasks
- Do not swallow exceptions — log and rethrow or convert to a domain-specific error
- Use
Task.FromException<T>() (or ValueTask.FromException) to return a pre-faulted task instead of throwing before the first await in a task-returning method — this keeps the caller's exception-handling path consistent
- In library code, use
ConfigureAwait(false) after every await to avoid capturing the synchronization context (see ConfigureAwait section below)
ConfigureAwait
The ConfigureAwait decision depends on where your code runs:
- Library code (NuGet packages, shared class libraries): use
.ConfigureAwait(false) on every await. Library code should not assume a synchronization context exists, and capturing one unnecessarily can cause deadlocks when callers block with .Result or .Wait().
- Application-level code (ASP.NET Core controllers, Blazor components, console apps): omit
ConfigureAwait(false). ASP.NET Core has no SynchronizationContext, so it is a no-op there, but in UI frameworks (WPF, WinForms, MAUI) the default behavior of resuming on the UI thread is what you want.
Parallel and Concurrent Execution
- Use
Task.WhenAll() to run independent tasks concurrently and await all of them
- Use
Task.WhenAny() for racing tasks (e.g., timeout patterns, first-response-wins)
- Avoid unnecessary
async/await when the method simply returns another task — just return the Task directly (but keep async if you need try/catch or using around the await)
- Use
Task.Run() only to offload CPU-bound work to the thread pool — never wrap purely async I/O in Task.Run()
Async Streams (IAsyncEnumerable)
Use IAsyncEnumerable<T> to produce or consume sequences of data asynchronously (e.g., database cursor results, paginated API calls, streaming file reads):
async IAsyncEnumerable<Item> GetItemsAsync(
[EnumeratorCancellation] CancellationToken ct = default)
{
await foreach (var row in db.QueryAsync(ct))
{
yield return Map(row);
}
}
- Apply
[EnumeratorCancellation] to the CancellationToken parameter so callers can pass a token via WithCancellation()
- Use
await foreach to consume async streams
Resource Disposal (IAsyncDisposable)
When a resource performs I/O during cleanup (flushing buffers, closing network connections), use async disposal to avoid blocking a thread:
- Implement
IAsyncDisposable on classes that hold async resources (database connections, HTTP clients, streams)
- Prefer
await using over using whenever the type implements IAsyncDisposable:
await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync(cancellationToken);
- If a class implements both
IDisposable and IAsyncDisposable, await using will call DisposeAsync() — which is the one you want in async code paths
Async Synchronization
Standard locks (lock, Monitor) cannot be held across an await because the continuation may run on a different thread. Use async-aware synchronization primitives instead:
- Use
SemaphoreSlim (with await semaphore.WaitAsync(ct)) as an async-compatible mutex or throttle
- Use
Channel<T> for async producer-consumer patterns — it is allocation-efficient and back-pressure-aware
Common Pitfalls
| Anti-pattern |
Why it is dangerous |
.Wait(), .Result, .GetAwaiter().GetResult() |
Blocks the calling thread and can deadlock when a SynchronizationContext exists. Use await instead. |
async void methods (non-event-handler) |
Exceptions go unobserved and crash the process. Return Task so the caller can await and catch. |
| Mixing sync and async |
Calling async from sync (or vice versa) introduces deadlock risk and thread-pool starvation. Keep the call chain consistently async. |
| Forgetting to await a Task |
The task runs fire-and-forget; exceptions are silently lost and execution order becomes unpredictable. |
Task.Run() wrapping async I/O |
Wastes a thread-pool thread that just waits on I/O. Call the async method directly. |
Implementation Patterns
- TAP (Task-based Asynchronous Pattern): the standard for public async APIs in .NET — return
Task/Task<T> and accept a CancellationToken
- Async command pattern: for long-running operations that need progress reporting and cancellation
- Async factory methods: when construction requires async work (e.g., opening a connection), expose a static
CreateAsync() method rather than doing async work in the constructor
When reviewing C# code, identify async anti-patterns from this guide and suggest concrete improvements with corrected code.
1---2name: csharp-async3description: Best practices and patterns for C# async/await programming including Task and ValueTask usage, CancellationToken propagation, IAsyncDisposable and await using, ConfigureAwait, async streams (IAsyncEnumerable), parallel task execution, and common pitfalls like deadlocks and async void. Use this skill whenever writing, reviewing, or refactoring asynchronous C# code — including when the user mentions async methods, Task-returning APIs, cancellation, deadlocks, blocking on async code, or resource disposal in async contexts, even if they do not explicitly say "async best practices."4---56# C# Async Programming Best Practices78Apply these practices when writing, reviewing, or refactoring asynchronous C# code.910## Naming Conventions1112- Suffix all async methods with `Async` (e.g., `GetDataAsync` for a synchronous `GetData`)13- Match names with synchronous counterparts when both exist1415## Return Types1617- Return `Task<T>` when the method produces a value18- Return `Task` when the method has no return value19- Use `ValueTask<T>` when the method frequently completes synchronously (e.g., cache hits, short-circuit returns) — this avoids a `Task` heap allocation on the hot path20- Never return `async void` except in event handlers — `async void` methods swallow exceptions and cannot be awaited, making errors invisible to the caller2122## CancellationToken Propagation2324CancellationTokens are the cooperative cancellation mechanism in .NET. Dropping a token silently means the user loses the ability to cancel an operation that might be long-running or expensive.2526- Accept `CancellationToken` as the **last parameter** in every async method signature27- Always forward the token through the entire call chain — never silently drop it28- Pass tokens to every framework method that accepts one (`HttpClient`, `Stream`, `DbCommand`, `Channel`, EF Core queries, etc.)29- Use `token.ThrowIfCancellationRequested()` inside long-running CPU loops to make them responsive to cancellation30- Catch `OperationCanceledException` at boundary layers (API controllers, hosted service entry points) — do not swallow it or rethrow as a different exception type31- Use `CancellationTokenSource.CreateLinkedTokenSource()` to combine scopes — for instance, merging a request-scoped token with a user-initiated cancel:3233```csharp34using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(requestToken, userCancelToken);35await ProcessAsync(linkedCts.Token);36```3738## Exception Handling3940- Wrap `await` expressions in try/catch to handle faulted tasks41- Do not swallow exceptions — log and rethrow or convert to a domain-specific error42- Use `Task.FromException<T>()` (or `ValueTask.FromException`) to return a pre-faulted task instead of throwing before the first `await` in a task-returning method — this keeps the caller's exception-handling path consistent43- In library code, use `ConfigureAwait(false)` after every `await` to avoid capturing the synchronization context (see ConfigureAwait section below)4445## ConfigureAwait4647The `ConfigureAwait` decision depends on where your code runs:4849- **Library code** (NuGet packages, shared class libraries): use `.ConfigureAwait(false)` on every `await`. Library code should not assume a synchronization context exists, and capturing one unnecessarily can cause deadlocks when callers block with `.Result` or `.Wait()`.50- **Application-level code** (ASP.NET Core controllers, Blazor components, console apps): omit `ConfigureAwait(false)`. ASP.NET Core has no `SynchronizationContext`, so it is a no-op there, but in UI frameworks (WPF, WinForms, MAUI) the default behavior of resuming on the UI thread is what you want.5152## Parallel and Concurrent Execution5354- Use `Task.WhenAll()` to run independent tasks concurrently and await all of them55- Use `Task.WhenAny()` for racing tasks (e.g., timeout patterns, first-response-wins)56- Avoid unnecessary `async`/`await` when the method simply returns another task — just return the `Task` directly (but keep `async` if you need try/catch or `using` around the await)57- Use `Task.Run()` only to offload CPU-bound work to the thread pool — never wrap purely async I/O in `Task.Run()`5859## Async Streams (IAsyncEnumerable)6061Use `IAsyncEnumerable<T>` to produce or consume sequences of data asynchronously (e.g., database cursor results, paginated API calls, streaming file reads):6263```csharp64async IAsyncEnumerable<Item> GetItemsAsync(65 [EnumeratorCancellation] CancellationToken ct = default)66{67 await foreach (var row in db.QueryAsync(ct))68 {69 yield return Map(row);70 }71}72```7374- Apply `[EnumeratorCancellation]` to the `CancellationToken` parameter so callers can pass a token via `WithCancellation()`75- Use `await foreach` to consume async streams7677## Resource Disposal (IAsyncDisposable)7879When a resource performs I/O during cleanup (flushing buffers, closing network connections), use async disposal to avoid blocking a thread:8081- Implement `IAsyncDisposable` on classes that hold async resources (database connections, HTTP clients, streams)82- Prefer `await using` over `using` whenever the type implements `IAsyncDisposable`:8384```csharp85await using var connection = new SqlConnection(connectionString);86await connection.OpenAsync(cancellationToken);87```8889- If a class implements both `IDisposable` and `IAsyncDisposable`, `await using` will call `DisposeAsync()` — which is the one you want in async code paths9091## Async Synchronization9293Standard locks (`lock`, `Monitor`) cannot be held across an `await` because the continuation may run on a different thread. Use async-aware synchronization primitives instead:9495- Use `SemaphoreSlim` (with `await semaphore.WaitAsync(ct)`) as an async-compatible mutex or throttle96- Use `Channel<T>` for async producer-consumer patterns — it is allocation-efficient and back-pressure-aware9798## Common Pitfalls99100| Anti-pattern | Why it is dangerous |101|---|---|102| `.Wait()`, `.Result`, `.GetAwaiter().GetResult()` | Blocks the calling thread and can deadlock when a `SynchronizationContext` exists. Use `await` instead. |103| `async void` methods (non-event-handler) | Exceptions go unobserved and crash the process. Return `Task` so the caller can await and catch. |104| Mixing sync and async | Calling async from sync (or vice versa) introduces deadlock risk and thread-pool starvation. Keep the call chain consistently async. |105| Forgetting to await a Task | The task runs fire-and-forget; exceptions are silently lost and execution order becomes unpredictable. |106| `Task.Run()` wrapping async I/O | Wastes a thread-pool thread that just waits on I/O. Call the async method directly. |107108## Implementation Patterns109110- **TAP (Task-based Asynchronous Pattern)**: the standard for public async APIs in .NET — return `Task`/`Task<T>` and accept a `CancellationToken`111- **Async command pattern**: for long-running operations that need progress reporting and cancellation112- **Async factory methods**: when construction requires async work (e.g., opening a connection), expose a static `CreateAsync()` method rather than doing async work in the constructor113114When reviewing C# code, identify async anti-patterns from this guide and suggest concrete improvements with corrected code.