How to handle exceptions with asynchronous in C#

By FoxLearn 1/3/2025 8:05:00 AM   329
Exception handling in asynchronous code differs from synchronous methods in C#.

While asynchronous programming allows for non-blocking execution of resource-intensive operations, handling errors in such methods requires a different approach.

This article explains the specific exception handling mechanisms for asynchronous methods in C#, highlighting how errors should be managed when working with async operations.

Exception handling in asynchronous vs synchronous

In synchronous C# code, exceptions are propagated up the call stack until they reach a suitable catch block. However, exception handling in asynchronous methods is more complex.

Asynchronous methods with a return type of Task or Task<T> wrap any exceptions in an AggregateException and attach it to the Task object. If multiple exceptions occur, they are all stored within the Task.

For async methods with a void return type, no Task object is involved. In this case, exceptions are raised on the active SynchronizationContext at the time the async method was called.

Exceptions in asynchronous methods that return void

Consider a program where an asynchronous method returns void and throws an exception:

public static void RunTest()
{
    try
    {
        PerformAsyncOperation();
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Error caught: {ex.Message}");
    }
}

private static async void PerformAsyncOperation()
{
    throw new Exception("Something went wrong during the async operation.");
}

When you run the RunTest() method, you’ll notice that the exception is not caught by the catch block in RunTest(). This is because PerformAsyncOperation() returns void and exceptions thrown inside it are not automatically propagated to the calling method’s exception handling mechanism.

Exceptions in asynchronous methods that return a Task object

Consider a scenario where multiple exceptions are thrown in asynchronous tasks that return Task objects. The exceptions are wrapped in an AggregateException and returned to the calling method. When awaiting the task, only the first exception is exposed from the list of exceptions.

public static async Task HandleMultipleExceptionsAsync()
{
    try
    {
        var task1 = Task.Run(() => throw new NullReferenceException("NullReferenceException occurred."));
        var task2 = Task.Run(() => throw new InvalidOperationException("InvalidOperationException occurred."));
        
        await Task.WhenAll(task1, task2);
    }
    catch (AggregateException ex)
    {
        Console.WriteLine($"Caught AggregateException: {ex.Message}");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Caught Exception: {ex.Message}");
    }
}

When you run the HandleMultipleExceptionsAsync() method, you will notice that the message "NullReferenceException occurred." will be displayed in the console. Even though both task1 and task2 throw exceptions, only the first exception (NullReferenceException) is caught and displayed. The exceptions are wrapped inside an AggregateException, but only the first one is handled by the catch (Exception) block after the await.

How to use AggregateException.Handle to handle exceptions in C#

Consider the following program where multiple exceptions are thrown, and we handle only the specific ones we are interested in:

public async static Task HandleSelectedExceptionsAsync()
{
    try
    {
        var task1 = Task.Run(() => throw new ArgumentNullException("ArgumentNullException is thrown."));
        var task2 = Task.Run(() => throw new FileNotFoundException("FileNotFoundException is thrown."));
        
        Task.WaitAll(task1, task2);
    }
    catch (AggregateException ae)
    {
        ae.Handle(ex =>
        {
            if (ex is ArgumentNullException)
            {
                Console.WriteLine($"Handled: {ex.Message}");
                return true; // Handled successfully
            }

            // Ignore FileNotFoundException
            if (ex is FileNotFoundException)
            {
                Console.WriteLine($"Ignored: {ex.Message}");
                return false; // Not handled, continue
            }

            return false; // Not handled
        });
    }
}

In this example, the ArgumentNullException is handled and printed to the console, while the FileNotFoundException is ignored. The Handle method of AggregateException is used to specify which exceptions to process and which ones to ignore.

Finally, you can invoke this method from your Main function like this:

static async Task Main(string[] args)
{
    await HandleSelectedExceptionsAsync();
    Console.Read();
}

This example demonstrates how you can leverage asynchronous programming to make applications scalable and responsive. When working with async methods, remember that the exception handling logic differs from that of synchronous code, allowing you to manage specific errors selectively.