How do you handle exceptions in async methods?

4 minintermediate.NETasync-awaitexception-handling

Quick Answer

Exceptions in async methods are captured and re-thrown when the returned task is awaited, so normal try/catch around the `await` works. With `Task.WhenAll`, awaiting throws only the first exception, but the task's `Exception` holds all of them as an `AggregateException`. Avoid `async void` because its exceptions can't be caught by the caller and crash the process.

Detailed Answer

Exception handling in async methods uses try-catch blocks, but with important considerations for how exceptions are propagated.

Basic exception handling:

public async Task ProcessDataAsync()
{
    try
    {
        var data = await FetchDataAsync();
        await SaveDataAsync(data);
    }
    catch (HttpRequestException ex)
    {
        // Handle network errors
        _logger.LogError(ex, "Network error occurred");
        throw;
    }
    catch (Exception ex)
    {
        // Handle other errors
        _logger.LogError(ex, "Unexpected error");
        throw;
    }
    finally
    {
        // Cleanup code always runs
        _logger.LogInformation("Processing completed");
    }
}

Handling exceptions with Task.WhenAll():

When using Task.WhenAll(), only the first exception is thrown. To get all exceptions:

public async Task ProcessMultipleAsync()
{
    var tasks = new[]
    {
        ProcessItem1Async(),
        ProcessItem2Async(),
        ProcessItem3Async()
    };
    
    try
    {
        await Task.WhenAll(tasks);
    }
    catch (Exception ex)
    {
        // Only first exception is caught here
        _logger.LogError(ex, "At least one task failed");
        
        // To get ALL exceptions:
        foreach (var task in tasks)
        {
            if (task.IsFaulted)
            {
                foreach (var exception in task.Exception.InnerExceptions)
                {
                    _logger.LogError(exception, "Task exception");
                }
            }
        }
    }
}

Fire-and-forget pattern (dangerous but sometimes necessary):

// BAD: Exception will crash the application
public void StartBackgroundWork()
{
    _ = DoWorkAsync(); // Fire and forget - DON'T DO THIS
}

// GOOD: Properly handled fire-and-forget
public void StartBackgroundWork()
{
    _ = SafeFireAndForgetAsync(DoWorkAsync());
}

private async Task SafeFireAndForgetAsync(Task task)
{
    try
    {
        await task;
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Background task failed");
    }
}

Exception handling with ConfigureAwait:

public async Task ProcessAsync()
{
    try
    {
        // ConfigureAwait doesn't affect exception handling
        var result = await FetchDataAsync().ConfigureAwait(false);
        await SaveAsync(result).ConfigureAwait(false);
    }
    catch (Exception ex)
    {
        // Exceptions are still caught normally
        _logger.LogError(ex, "Error in processing");
    }
}

Key principles:

  • Always await async methods to catch exceptions
  • Use try-catch around await statements
  • Log exceptions before rethrowing
  • Be aware of AggregateException with Task.WhenAll()
  • Never ignore exceptions in fire-and-forget scenarios