C#: async/await & Tasks
C# async/await is built on the Task Parallel Library (TPL). async/await transforms methods into state machines that resume on completion — no thread is blocked while awaiting.
async/await Basics
// Async method — always returns Task, Task<T>, or ValueTask<T>
public async Task<string> FetchDataAsync(string url)
{
using var client = new HttpClient();
var response = await client.GetAsync(url); // await — no blocking
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
// Async void — only for event handlers (errors are unobservable)
private async void Button_Click(object sender, EventArgs e)
{
var data = await FetchDataAsync("https://api.example.com");
label.Text = data;
}
// ConfigureAwait(false) — don't capture sync context (use in library code)
var result = await SomeMethodAsync().ConfigureAwait(false);
// CancellationToken — allow callers to cancel
public async Task<List<User>> GetUsersAsync(CancellationToken ct = default)
{
await Task.Delay(100, ct); // throws OperationCanceledException if cancelled
return await _db.Users.ToListAsync(ct);
}
// Call with cancellation
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var users = await GetUsersAsync(cts.Token);Task Parallelism
// WhenAll — run multiple tasks concurrently, wait for all
var task1 = FetchUsersAsync();
var task2 = FetchOrdersAsync();
var task3 = FetchStatsAsync();
await Task.WhenAll(task1, task2, task3);
var users = await task1; // already completed — no extra wait
// Shorthand
var (users, orders, stats) = await (task1, task2, task3); // with tuple await
// WhenAny — return first completed task (timeout pattern)
var dataTask = FetchDataAsync();
var timeoutTask = Task.Delay(TimeSpan.FromSeconds(5));
var completed = await Task.WhenAny(dataTask, timeoutTask);
if (completed == timeoutTask)
throw new TimeoutException("Request timed out");
var data = await dataTask;
// Parallel.ForEachAsync (C# 6+ / .NET 6)
await Parallel.ForEachAsync(ids, new ParallelOptions { MaxDegreeOfParallelism = 10 }, async (id, ct) => {
await ProcessAsync(id, ct);
});
// Channel — producer/consumer async queue
var channel = Channel.CreateBounded<WorkItem>(capacity: 100);
var writer = channel.Writer;
var reader = channel.Reader;
// Producer
await writer.WriteAsync(new WorkItem());
// Consumer
await foreach (var item in reader.ReadAllAsync()) {
await ProcessItem(item);
}Exception Handling in Async Code
// Standard try/catch works with await
public async Task ProcessAsync()
{
try {
var result = await FetchDataAsync();
await SaveAsync(result);
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound) {
_logger.LogWarning("Resource not found: {Message}", ex.Message);
}
catch (OperationCanceledException) {
_logger.LogInformation("Operation was cancelled");
throw; // re-throw cancellation
}
catch (Exception ex) {
_logger.LogError(ex, "Unexpected error");
throw;
}
finally {
Cleanup(); // always runs
}
}
// Task.WhenAll throws AggregateException with all errors
try {
await Task.WhenAll(task1, task2, task3);
}
catch (Exception) {
// Check all task exceptions
var exceptions = new[] { task1, task2, task3 }
.Where(t => t.IsFaulted)
.Select(t => t.Exception?.InnerException)
.ToList();
}
// ValueTask<T> — avoid allocation when result is often synchronous
public ValueTask<int> GetCachedValueAsync(string key)
{
if (_cache.TryGetValue(key, out var cached))
return ValueTask.FromResult(cached); // no allocation
return new ValueTask<int>(FetchAndCacheAsync(key)); // async path
}Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free