csharp-async
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."
From dotnetnpx claudepluginhub atc-net/atc-agentic-toolkit --plugin dotnetThis skill uses the workspace's default tool permissions.
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.,GetDataAsyncfor a synchronousGetData) - Match names with synchronous counterparts when both exist
Return Types
- Return
Task<T>when the method produces a value - Return
Taskwhen the method has no return value - Use
ValueTask<T>when the method frequently completes synchronously (e.g., cache hits, short-circuit returns) — this avoids aTaskheap allocation on the hot path - Never return
async voidexcept in event handlers —async voidmethods 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
CancellationTokenas 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
OperationCanceledExceptionat 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
awaitexpressions 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>()(orValueTask.FromException) to return a pre-faulted task instead of throwing before the firstawaitin a task-returning method — this keeps the caller's exception-handling path consistent - In library code, use
ConfigureAwait(false)after everyawaitto 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 everyawait. Library code should not assume a synchronization context exists, and capturing one unnecessarily can cause deadlocks when callers block with.Resultor.Wait(). - Application-level code (ASP.NET Core controllers, Blazor components, console apps): omit
ConfigureAwait(false). ASP.NET Core has noSynchronizationContext, 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/awaitwhen the method simply returns another task — just return theTaskdirectly (but keepasyncif you need try/catch orusingaround the await) - Use
Task.Run()only to offload CPU-bound work to the thread pool — never wrap purely async I/O inTask.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 theCancellationTokenparameter so callers can pass a token viaWithCancellation() - Use
await foreachto 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
IAsyncDisposableon classes that hold async resources (database connections, HTTP clients, streams) - Prefer
await usingoverusingwhenever the type implementsIAsyncDisposable:
await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync(cancellationToken);
- If a class implements both
IDisposableandIAsyncDisposable,await usingwill callDisposeAsync()— 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(withawait 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 aCancellationToken - 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.