⚡ Async Done Right
Async/await is powerful but tricky. Common mistakes cause deadlocks, performance issues, bugs. Learn to do it right.
❌ Mistake 1: Async Void
// ❌ BAD: Async void (only for event handlers!)
public async void ProcessData()
{
await DoWorkAsync();
}
// Problems:
// - Can't await it
// - Exceptions crash app
// - No way to know when it completes
// ✅ GOOD: Async Task
public async Task ProcessDataAsync()
{
await DoWorkAsync();
}
// Can await, catch exceptions, compose operations
// Exception: Event handlers MUST be async void
private async void Button_Click(object sender, EventArgs e)
{
try
{
await ProcessDataAsync();
}
catch (Exception ex)
{
// Handle exception - can't let it bubble up
Logger.LogError(ex);
}
}
❌ Mistake 2: Blocking on Async (Deadlock)
// ❌ BAD: Deadlock in UI or ASP.NET (pre-Core)
public void ProcessData()
{
var result = GetDataAsync().Result; // DEADLOCK!
// Or
GetDataAsync().Wait(); // DEADLOCK!
}
async Task GetDataAsync()
{
await Task.Delay(1000);
return "data";
}
// Why deadlock?
// 1. .Result blocks current thread
// 2. await tries to resume on same thread
// 3. Thread is blocked waiting for await
// 4. Deadlock!
// ✅ GOOD: Async all the way
public async Task ProcessDataAsync()
{
var result = await GetDataAsync();
}
// Or in console app / ASP.NET Core (no sync context)
public void Main()
{
var result = GetDataAsync().GetAwaiter().GetResult(); // OK in console
}
❌ Mistake 3: Not Using ConfigureAwait(false)
// ❌ In library code: Captures context unnecessarily
public async Task GetDataAsync()
{
await httpClient.GetAsync(url); // Resumes on original context
return data;
}
// ✅ GOOD: Library code should use ConfigureAwait(false)
public async Task GetDataAsync()
{
await httpClient.GetAsync(url).ConfigureAwait(false);
return data;
}
// Rule of thumb:
// - UI code: Don't use ConfigureAwait (need UI thread)
// - Library code: Always ConfigureAwait(false) (performance)
// - ASP.NET Core: Doesn't matter (no sync context)
❌ Mistake 4: Fire and Forget
// ❌ BAD: Exceptions swallowed
public void DoWork()
{
_ = ProcessDataAsync(); // Fire and forget - exception lost!
}
// ✅ GOOD: Await or explicitly handle
public async Task DoWorkAsync()
{
await ProcessDataAsync();
}
// Or if truly fire-and-forget, handle exceptions
public void DoWork()
{
_ = ProcessDataSafelyAsync();
}
private async Task ProcessDataSafelyAsync()
{
try
{
await ProcessDataAsync();
}
catch (Exception ex)
{
Logger.LogError(ex);
}
}
✅ Parallel Async Operations
// ❌ BAD: Sequential (6 seconds total)
var user = await GetUserAsync(); // 2 seconds
var orders = await GetOrdersAsync(); // 2 seconds
var products = await GetProductsAsync(); // 2 seconds
// ✅ GOOD: Parallel (2 seconds total)
var userTask = GetUserAsync();
var ordersTask = GetOrdersAsync();
var productsTask = GetProductsAsync();
await Task.WhenAll(userTask, ordersTask, productsTask);
var user = await userTask;
var orders = await ordersTask;
var products = await productsTask;
// Or even better
var (user, orders, products) = await (
GetUserAsync(),
GetOrdersAsync(),
GetProductsAsync()
);
✅ Cancellation Tokens
// Always accept CancellationToken
public async Task GetDataAsync(CancellationToken ct = default)
{
// Pass to async operations
var response = await httpClient.GetAsync(url, ct);
// Check periodically in loops
foreach (var item in items)
{
ct.ThrowIfCancellationRequested();
await ProcessAsync(item, ct);
}
return data;
}
// Usage with timeout
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var data = await GetDataAsync(cts.Token);
✅ Golden Rules
- Async all the way: Don’t block on async code
- Return Task, not void: Except event handlers
- Use ConfigureAwait(false): In library code
- Accept CancellationToken: For all async methods
- Parallel when independent: Use Task.WhenAll
- Suffix with Async: Name methods FooAsync
⚠️ Common Traps
// ❌ Capturing loop variable
foreach (var item in items)
{
tasks.Add(Task.Run(() => Process(item))); // Captures 'item'!
}
// ✅ Copy to local variable
foreach (var item in items)
{
var local = item;
tasks.Add(Task.Run(() => Process(local)));
}
// ❌ Async lambda in LINQ (doesn't await!)
items.Select(async item => await ProcessAsync(item));
// ✅ Use Task.WhenAll
var tasks = items.Select(item => ProcessAsync(item));
await Task.WhenAll(tasks);
“App froze randomly. Found .Result blocking async code. Changed to async all the way. No more freezes. Then made parallel calls with Task.WhenAll. API response time: 5s → 1s. Async is powerful when done right.”
