C# Async Patterns
Master async/await and asynchronous programming patterns.
By EMEPublished: February 20, 2025
csharpasyncawaitasynchronouspatterns
A Simple Analogy
Async is like ordering food at a restaurant. Instead of waiting at the counter, you get a number and do other things. When your food is ready (the Task completes), you pick it up.
Why Use Async?
- Responsiveness: UI doesn't freeze during I/O
- Scalability: Handle more concurrent requests
- Efficiency: Don't block threads while waiting
- Better UX: Apps feel snappier
- Resource savings: Fewer threads = lower memory
Basic Async/Await
// Without async: Blocks thread
public string FetchData()
{
var data = new WebClient().DownloadString("https://api.example.com/data");
return data;
}
// With async: Non-blocking
public async Task<string> FetchDataAsync()
{
var client = new HttpClient();
var data = await client.GetStringAsync("https://api.example.com/data");
return data;
}
// Usage
var result = await FetchDataAsync();
Async Patterns
// Pattern 1: Fire and forget (bad practice)
_ = LoadDataAsync(); // Ignores exceptions
// Pattern 2: Async all the way
public async Task ProcessOrderAsync(Order order)
{
await ValidateOrderAsync(order);
await SaveOrderAsync(order);
await SendEmailAsync(order.Email);
}
// Pattern 3: Parallel async operations
public async Task<(Product, Reviews)> GetProductDetailsAsync(int id)
{
var productTask = GetProductAsync(id);
var reviewsTask = GetReviewsAsync(id);
await Task.WhenAll(productTask, reviewsTask);
return (productTask.Result, reviewsTask.Result);
}
// Pattern 4: Cancellation
public async Task<Data> FetchWithCancellationAsync(CancellationToken ct)
{
var response = await httpClient.GetAsync("https://api.example.com/data", ct);
return await response.Content.ReadAsAsync<Data>();
}
Exception Handling
// Try-catch works with async
public async Task<Order> GetOrderAsync(int id)
{
try
{
return await orderService.GetAsync(id);
}
catch (HttpRequestException ex)
{
logger.LogError("API error: {0}", ex.Message);
throw;
}
}
// Multiple async operations with error handling
public async Task ProcessMultipleAsync(List<int> ids)
{
var tasks = ids.Select(id => ProcessOneAsync(id)).ToList();
try
{
await Task.WhenAll(tasks);
}
catch (Exception ex)
{
var failed = tasks.Where(t => t.IsFaulted).ToList();
logger.LogError("Failed tasks: {0}", failed.Count);
}
}
ValueTask for Performance
// Use Task for I/O operations
public async Task<string> FetchFromApiAsync()
{
var response = await httpClient.GetAsync("https://api.example.com");
return await response.Content.ReadAsStringAsync();
}
// Use ValueTask when result is often synchronous
public async ValueTask<int> GetCachedValueAsync(string key)
{
// Often returns synchronously from cache
if (cache.TryGetValue(key, out var value))
return value;
// Occasionally awaits database
return await database.GetValueAsync(key);
}
Best Practices
- Async all the way: Make callers async too
- Avoid blocking: Never use
.Resultor.Wait() - Configure await: Use
ConfigureAwait(false)in libraries - Handle cancellation: Accept
CancellationToken - Use ValueTask: When result is often synchronous
Related Concepts
- Task vs. ValueTask performance
- Async iterators
- Task schedulers
- Synchronization context
- Async streams
Summary
C# async patterns enable responsive, scalable applications by preventing thread blocking. Master async/await to build modern, efficient applications.