C#idiomatic C#
async-awaitidioms

Idioms

Async and await

Await everything, block nothing: the shape async code takes when it is written to be awaited.

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

csharp
string text = await client.GetStringAsync(url);

Avoid

csharp
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

csharp
async Task<Report> LoadAsync(int id)
{
    var rows = await _store.ReadAsync(id);
    return Report.From(rows);
}

Avoid

csharp
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

csharp
Task<byte[]> ReadAllBytesAsync(string path)

Avoid

csharp
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

csharp
ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken ct)

Avoid

csharp
var pending = ReadAsync(buffer, ct);
// awaited later, and maybe again after that

Corpus: 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

csharp
private async void Save_Click(object sender, EventArgs e)
{
    await SaveAsync();
}

Avoid

csharp
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

csharp
await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ct);

Avoid

csharp
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

csharp
var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);

Avoid

csharp
var json = await response.Content.ReadAsStringAsync();

Corpus: CA2007: Do not directly await a Task

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

csharp
var users = GetUsersAsync(ct);
var orders = GetOrdersAsync(ct);
await Task.WhenAll(users, orders);

Avoid

csharp
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

csharp
var hash = await Task.Run(() => ComputeHash(buffer), ct);

Avoid

csharp
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

csharp
await Task.Delay(TimeSpan.FromSeconds(2), ct);

Avoid

csharp
Thread.Sleep(2000);

Corpus: Asynchronous programming with async and await

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

csharp
await foreach (var row in _store.ReadRowsAsync(ct).WithCancellation(ct))
{
    Console.WriteLine(row.Id);
}

Avoid

csharp
Task<List<Row>> ReadRowsAsync(CancellationToken ct);

Corpus: Generate and consume asynchronous streams

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

csharp
_pending.Add(RefreshAsync(ct));
await Task.WhenAll(_pending);

Avoid

csharp
_ = RefreshAsync(ct); // the exception it throws goes nowhere

Corpus: Async scenarios