15 minlesson

Exception Handling in Async Code

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:

csharp
1async Task ProcessDataAsync()
2{
3 try
4 {
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:

csharp
1async Task ExampleAsync()
2{
3 Task task = FailingMethodAsync(); // Exception NOT thrown here
4
5 // ... other code runs ...
6
7 await task; // Exception thrown HERE when awaited
8}
9
10async 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:

csharp
1async Task<int> FailAsync()
2{
3 await Task.Delay(100);
4 throw new InvalidOperationException("Failed!");
5}
6
7Task<int> task = FailAsync(); // Exception captured, not thrown
8
9// Option 1: Await - throws the exception
10try
11{
12 int result = await task;
13}
14catch (InvalidOperationException ex)
15{
16 // Exception caught here
17}
18
19// Option 2: Check the Task
20if (task.IsFaulted)
21{
22 AggregateException ae = task.Exception;
23 // ae.InnerExceptions contains the actual exceptions
24}

AggregateException

Tasks wrap exceptions in AggregateException, but await unwraps it:

csharp
1// await unwraps to the actual exception
2try
3{
4 await FailingTask();
5}
6catch (InvalidOperationException ex) // Original exception
7{
8 Console.WriteLine(ex.Message);
9}
10
11// .Wait() and .Result throw AggregateException
12try
13{
14 FailingTask().Wait();
15}
16catch (AggregateException ae) // Wrapped exception
17{
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:

csharp
1async 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 succeeds
6
7 try
8 {
9 await Task.WhenAll(task1, task2, task3);
10 }
11 catch (Exception ex)
12 {
13 // Only the FIRST exception is thrown by await
14 Console.WriteLine($"Caught: {ex.GetType().Name}");
15 }
16}

Getting All Exceptions from WhenAll

csharp
1var tasks = new[]
2{
3 FailWithMessageAsync("Error 1"),
4 FailWithMessageAsync("Error 2"),
5 SucceedAsync()
6};
7
8Task allTasks = Task.WhenAll(tasks);
9
10try
11{
12 await allTasks;
13}
14catch
15{
16 // Get ALL exceptions
17 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:

csharp
1async Task ProcessWithCleanupAsync()
2{
3 var resource = await AcquireResourceAsync();
4 try
5 {
6 await UseResourceAsync(resource);
7 }
8 finally
9 {
10 // Always runs, even if exception thrown
11 await resource.ReleaseAsync();
12 }
13}

async void Exception Behavior

Exceptions in async void methods crash the application!

csharp
1// DANGEROUS - exception crashes app
2async void BadMethod()
3{
4 await Task.Delay(100);
5 throw new Exception("This crashes the app!");
6}
7
8// Can't catch it!
9try
10{
11 BadMethod(); // Can't await
12}
13catch
14{
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:

csharp
1async Task ProcessAsync()
2{
3 try
4 {
5 await RiskyOperationAsync();
6 }
7 catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
8 {
9 // Handle 404 specifically
10 Console.WriteLine("Resource not found");
11 }
12 catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)
13 {
14 // Handle 401 specifically
15 await RefreshTokenAsync();
16 await ProcessAsync(); // Retry
17 }
18}

Rethrowing Exceptions

Use throw; to preserve the stack trace:

csharp
1async Task ProcessAsync()
2{
3 try
4 {
5 await RiskyOperationAsync();
6 }
7 catch (Exception ex)
8 {
9 LogError(ex);
10 throw; // Preserves stack trace
11 // throw ex; // BAD - loses original stack trace
12 }
13}

Best Practices

1. Use try/catch with await

csharp
1try
2{
3 await DoWorkAsync();
4}
5catch (SpecificException ex)
6{
7 // Handle specific exception
8}

2. Avoid async void

csharp
1// BAD
2async void DoWork() { ... }
3
4// GOOD
5async Task DoWorkAsync() { ... }

3. Don't swallow exceptions silently

csharp
1// BAD
2try
3{
4 await DoWorkAsync();
5}
6catch { } // Silent failure!
7
8// GOOD
9try
10{
11 await DoWorkAsync();
12}
13catch (Exception ex)
14{
15 _logger.LogError(ex, "Operation failed");
16 throw; // Or handle appropriately
17}

4. Handle WhenAll exceptions properly

csharp
1var allTask = Task.WhenAll(tasks);
2try
3{
4 await allTask;
5}
6catch
7{
8 // Check allTask.Exception for all errors
9}

Key Takeaways

  • try/catch works naturally with await
  • Exceptions are thrown at the await point, not when the task is created
  • await unwraps AggregateException to the inner exception
  • Task.WhenAll only throws the first exception by default
  • async void exceptions crash the application
  • Use throw; to preserve stack traces when rethrowing
Exception Handling in Async Code - Anko Academy