C#
04 / 07

async/await & Tasks

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