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