Asynchronous code in C# is the default, not an optimization. These are the decisions that keep it correct: what to await, what to return, and what to hand a caller who may cancel.
Await the task instead of blocking on itCA1849
Call the async method and await it. Reading .Result, calling .Wait(), or using .GetAwaiter().GetResult() holds a thread while the work is not running, and in an application with a synchronization context it can deadlock outright.
Preferred
string text = await client.GetStringAsync(url);Avoid
string text = client.GetStringAsync(url).Result;Corpus: CA1849: Call async methods when in an async method · Async scenarios
Let async travel up the call chain
The first method that awaits makes its callers awaiters too. Mark them async and pass the task on, rather than hiding the asynchrony behind a blocking wrapper that the caller cannot see.
Preferred
async Task<Report> LoadAsync(int id)
{
var rows = await _store.ReadAsync(id);
return Report.From(rows);
}Avoid
Report Load(int id)
{
return LoadAsync(id).GetAwaiter().GetResult();
}Corpus: Async scenarios
Suffix the name with Async
An Async suffix is the convention the framework is built on, so an async method without one reads like a blocking method. Keep the suffix on the method that returns the task, and only on methods that are actually awaitable.
Preferred
Task<byte[]> ReadAllBytesAsync(string path)Avoid
Task<byte[]> ReadAllBytes(string path)Corpus: Task-based asynchronous pattern
Pick the lightest return type that fitsCA2012
Task and Task<T> cover almost everything. ValueTask<T> saves an allocation where the result is usually available synchronously, but it may be awaited once and must not be stored, cached, or consumed twice.
Preferred
ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken ct)Avoid
var pending = ReadAsync(buffer, ct);
// awaited later, and maybe again after thatCorpus: Async return types · CA2012: Use ValueTasks correctly
Reserve async void for event handlers
An async void method cannot be awaited, so its exceptions reach the synchronization context instead of the caller, which usually means the process ends. Anything that is not an event handler returns Task.
Preferred
private async void Save_Click(object sender, EventArgs e)
{
await SaveAsync();
}Avoid
public async void SaveAsync()
{
await _store.WriteAsync(_state);
}Corpus: Async return types
Take a token, forward the tokenCA2016
A long-running operation accepts a CancellationToken and passes it to every method that takes one. Check it in your own loops, but let the callee see it: a token checked only at the call site cannot cancel work that has already started.
Preferred
await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ct);Avoid
ct.ThrowIfCancellationRequested();
await _client.SendAsync(request);Corpus: CA2016: Forward the CancellationToken parameter · Cancel an async task or a list of tasks
ConfigureAwait(false) in library codeCA2007
A library has no business resuming on the caller's UI thread. ConfigureAwait(false) on every await keeps the continuation on the pool; application code that needs its context back simply omits it.
Preferred
var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);Avoid
var json = await response.Content.ReadAsStringAsync();Start independent work together
Awaits written one after another run one after another. Start the tasks first, then await them as a group, so the total wait is the longest task instead of the sum of all of them.
Preferred
var users = GetUsersAsync(ct);
var orders = GetOrdersAsync(ct);
await Task.WhenAll(users, orders);Avoid
var users = await GetUsersAsync(ct);
var orders = await GetOrdersAsync(ct);Corpus: Start multiple async tasks and process them as they complete
Task.Run is for CPU-bound work
Task.Run moves work to the pool, which helps only when the work is computing. Wrapping blocking I/O in it consumes a thread to do what the async API would do without one.
Preferred
var hash = await Task.Run(() => ComputeHash(buffer), ct);Avoid
var text = await Task.Run(() => File.ReadAllText(path));Corpus: Async scenarios
Task.Delay replaces Thread.Sleep
Thread.Sleep blocks the thread it runs on. Task.Delay returns a task the caller can await or cancel, which is what makes a retry loop or a poll usable inside an async method.
Preferred
await Task.Delay(TimeSpan.FromSeconds(2), ct);Avoid
Thread.Sleep(2000);Stream with IAsyncEnumerable<T>
A sequence that arrives over time is an IAsyncEnumerable<T>, consumed with await foreach and produced with yield return. It is the one shape that lets a caller see items as they arrive and cancel between them.
Preferred
await foreach (var row in _store.ReadRowsAsync(ct).WithCancellation(ct))
{
Console.WriteLine(row.Id);
}Avoid
Task<List<Row>> ReadRowsAsync(CancellationToken ct);Never leave a task unobserved
A task that is started and never awaited loses its exception: nothing throws, nothing logs, and the work is silently gone. Await it, or hand it to something whose job is to track it.
Preferred
_pending.Add(RefreshAsync(ct));
await Task.WhenAll(_pending);Avoid
_ = RefreshAsync(ct); // the exception it throws goes nowhereCorpus: Async scenarios