Exception Handling in Async Code
Async methods handle exceptions differently than synchronous code. Understanding these differences is crucial for writing robust async applications.
Basic Exception Handling
The good news: try/catch works naturally with await:
csharp1async Task ProcessDataAsync()2{3 try4 {5 var data = await FetchDataAsync();6 await ProcessAsync(data);7 }8 catch (HttpRequestException ex)9 {10 Console.WriteLine($"Network error: {ex.Message}");11 }12 catch (Exception ex)13 {14 Console.WriteLine($"Unexpected error: {ex.Message}");15 }16}
When Exceptions Are Thrown
Exceptions from awaited tasks are thrown at the await point:
csharp1async Task ExampleAsync()2{3 Task task = FailingMethodAsync(); // Exception NOT thrown here45 // ... other code runs ...67 await task; // Exception thrown HERE when awaited8}910async Task FailingMethodAsync()11{12 await Task.Delay(100);13 throw new InvalidOperationException("Something went wrong");14}
Exceptions Are Captured in Tasks
When an async method throws, the exception is stored in the Task:
csharp1async Task<int> FailAsync()2{3 await Task.Delay(100);4 throw new InvalidOperationException("Failed!");5}67Task<int> task = FailAsync(); // Exception captured, not thrown89// Option 1: Await - throws the exception10try11{12 int result = await task;13}14catch (InvalidOperationException ex)15{16 // Exception caught here17}1819// Option 2: Check the Task20if (task.IsFaulted)21{22 AggregateException ae = task.Exception;23 // ae.InnerExceptions contains the actual exceptions24}
AggregateException
Tasks wrap exceptions in AggregateException, but await unwraps it:
csharp1// await unwraps to the actual exception2try3{4 await FailingTask();5}6catch (InvalidOperationException ex) // Original exception7{8 Console.WriteLine(ex.Message);9}1011// .Wait() and .Result throw AggregateException12try13{14 FailingTask().Wait();15}16catch (AggregateException ae) // Wrapped exception17{18 foreach (var ex in ae.InnerExceptions)19 {20 Console.WriteLine(ex.Message);21 }22}
Multiple Exceptions with WhenAll
Task.WhenAll can have multiple tasks fail:
csharp1async Task ProcessAllAsync()2{3 var task1 = Task.Run(() => throw new InvalidOperationException("Error 1"));4 var task2 = Task.Run(() => throw new ArgumentException("Error 2"));5 var task3 = Task.Run(() => 42); // This one succeeds67 try8 {9 await Task.WhenAll(task1, task2, task3);10 }11 catch (Exception ex)12 {13 // Only the FIRST exception is thrown by await14 Console.WriteLine($"Caught: {ex.GetType().Name}");15 }16}
Getting All Exceptions from WhenAll
csharp1var tasks = new[]2{3 FailWithMessageAsync("Error 1"),4 FailWithMessageAsync("Error 2"),5 SucceedAsync()6};78Task allTasks = Task.WhenAll(tasks);910try11{12 await allTasks;13}14catch15{16 // Get ALL exceptions17 if (allTasks.Exception != null)18 {19 foreach (var ex in allTasks.Exception.InnerExceptions)20 {21 Console.WriteLine($"Exception: {ex.Message}");22 }23 }24}
finally Still Works
The finally block executes as expected:
csharp1async Task ProcessWithCleanupAsync()2{3 var resource = await AcquireResourceAsync();4 try5 {6 await UseResourceAsync(resource);7 }8 finally9 {10 // Always runs, even if exception thrown11 await resource.ReleaseAsync();12 }13}
async void Exception Behavior
Exceptions in async void methods crash the application!
csharp1// DANGEROUS - exception crashes app2async void BadMethod()3{4 await Task.Delay(100);5 throw new Exception("This crashes the app!");6}78// Can't catch it!9try10{11 BadMethod(); // Can't await12}13catch14{15 // Never reached!16}
The exception goes to the SynchronizationContext or crashes the process.
Exception Filters with async
You can use exception filters with async:
csharp1async Task ProcessAsync()2{3 try4 {5 await RiskyOperationAsync();6 }7 catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)8 {9 // Handle 404 specifically10 Console.WriteLine("Resource not found");11 }12 catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)13 {14 // Handle 401 specifically15 await RefreshTokenAsync();16 await ProcessAsync(); // Retry17 }18}
Rethrowing Exceptions
Use throw; to preserve the stack trace:
csharp1async Task ProcessAsync()2{3 try4 {5 await RiskyOperationAsync();6 }7 catch (Exception ex)8 {9 LogError(ex);10 throw; // Preserves stack trace11 // throw ex; // BAD - loses original stack trace12 }13}
Best Practices
1. Use try/catch with await
csharp1try2{3 await DoWorkAsync();4}5catch (SpecificException ex)6{7 // Handle specific exception8}
2. Avoid async void
csharp1// BAD2async void DoWork() { ... }34// GOOD5async Task DoWorkAsync() { ... }
3. Don't swallow exceptions silently
csharp1// BAD2try3{4 await DoWorkAsync();5}6catch { } // Silent failure!78// GOOD9try10{11 await DoWorkAsync();12}13catch (Exception ex)14{15 _logger.LogError(ex, "Operation failed");16 throw; // Or handle appropriately17}
4. Handle WhenAll exceptions properly
csharp1var allTask = Task.WhenAll(tasks);2try3{4 await allTask;5}6catch7{8 // Check allTask.Exception for all errors9}
Key Takeaways
try/catchworks naturally withawait- Exceptions are thrown at the
awaitpoint, not when the task is created awaitunwrapsAggregateExceptionto the inner exceptionTask.WhenAllonly throws the first exception by defaultasync voidexceptions crash the application- Use
throw;to preserve stack traces when rethrowing