How to handle exceptions in asynchronous code in C#

Exception handling is different for asynchronous code. Learn the exception handling semantics for asynchronous methods in C#

How to handle exceptions in asynchronous code in C#
graemenicholson / Getty Images

Exception handling is the technique of handling runtime errors in an application. Asynchronous programming allows us to perform resource-intensive operations without the need for blocking on the main or executing thread of the application.

However, note that the error handling mechanism of an asynchronous method is different from that of a synchronous method. This article presents a discussion on how we can handle exceptions when working with asynchronous code in C#.

To work with the code examples provided in this article, you should have Visual Studio 2019 installed in your system. If you don’t already have a copy, you can download Visual Studio 2019 here

Create a .NET Core console application project in Visual Studio 2019

First off, let’s create a .NET Core Console Application project in Visual Studio. Assuming Visual Studio 2019 is installed in your system, follow the steps outlined below to create a new .NET Core Console Application project in Visual Studio 2019.

  1. Launch the Visual Studio IDE.
  2. Click “Create new project.”
  3. In the “Create new project” window, select “Console App (.NET Core)” from the list of templates displayed.
  4. Click Next.
  5. In the “Configure your new project” window shown next, specify the name and location for the new project.
  6. Click Create.

This will create a new .NET Core console application project in Visual Studio 2019. We’ll use this project in the subsequent sections of this article.

Exception handling in asynchronous vs. synchronous code

In synchronous C# code, the exceptions are propagated up the call stack until they reach an appropriate catch block that can handle the exception. However, exception handling in asynchronous methods is not as straightforward.

An asynchronous method in C# can have three types of return value: void, Task, and Task<TResult>.  When an exception occurs in an async method that has a return type of Task or Task<TResult>, the exception object is wrapped in an instance of AggregateException and attached to the Task object. If multiple exceptions are thrown, all of them are stored in the Task object.

However, asynchronous methods that have a return type of void don’t have a Task object associated with them. If exceptions are thrown in such methods, the exceptions are raised on the SynchronizationContext that was active at the time the asynchronous method was called.

[ Also on InfoWorld: .NET Framework APIs that won’t be coming to .NET 5.0 ]

Exceptions in asynchronous methods that return void

The following program shows an asynchronous method that returns void and throws an exception.

public static void ThisIsATestMethod()
        {
            try
            {
                AsyncMethodReturningVoid();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }
private static async void AsyncMethodReturningVoid()
        {
            throw new Exception("This is an error message...");
        }

When you execute the ThisIsATestMethod() method above, you’ll observe that the catch block of the calling method, i.e., ThisIsATestMethod, is not called.

Exceptions in asynchronous methods that return a Task object

When an exception is thrown in an asynchronous method that returns a Task object, the exceptions are wrapped in an instance of AggregateException and returned to the calling method. When we await the task, we get only the first exception from the list of exceptions that may have occurred. The following program illustrates this.

public static async Task ExceptionInAsyncCodeDemo()
        {
            try
            {
               var task1 = Task.Run(() => throw new IndexOutOfRangeException
                  ("IndexOutOfRangeException is thrown."));
               var task2 = Task.Run(() => throw new ArithmeticException
                  ("ArithmeticException is thrown."));
               await Task.WhenAll(task1, task2);
            }
               catch (AggregateException ex)
            {
               Console.WriteLine(ex.Message);
            }
               catch (Exception ex)
            {
               Console.WriteLine(ex.Message);
            }
        }

When you execute the above method, the message “IndexOutOfRangeException is thrown” will be displayed in the console window. Once the exception is thrown, the control will enter the second catch block, i.e., the catch block that uses Exception as its type parameter. Hence, although two exceptions have been thrown, we’ll receive only one of them.

Use the Exceptions property to retrieve all exceptions

To retrieve all of the exceptions that have been thrown, we can take advantage of the Exceptions property of the Task instance. The following code listing shows how we can retrieve all exceptions that have occurred in a method that returns a Task instance.

public static async Task ExceptionInAsyncCodeDemo()
        {
            Task tasks = null;
            try
            {
              var task1 = Task.Run(() => throw new IndexOutOfRangeException
                 ("IndexOutOfRangeException is thrown."));
              var task2 = Task.Run(() => throw new
                 ArithmeticException("ArithmeticException is thrown."));
              tasks = Task.WhenAll(task1, task2);
              await tasks;
            }
            catch
            {
                AggregateException aggregateException = tasks.Exception;
                foreach (var e in aggregateException.InnerExceptions)
                {
                    Console.WriteLine(e.GetType().ToString());
                }
            }
        }

Use AggregateException.Handle to handle exceptions

You can take advantage of AggregateException.Handle to handle certain exceptions while at the same time ignoring those exceptions that you don’t want to handle. The following code snippet shows how this can be achieved.

public async static Task ExceptionInAsyncCodeDemo()
        {
            try
            {
                var task1 = Task.Run(() => throw new
                   IndexOutOfRangeException
                   ("IndexOutOfRangeException is thrown."));
                var task2 = Task.Run(() => throw new
                   ArithmeticException
                   ("ArithmeticException is thrown."));
                Task.WaitAll(task1, task2);
            }
            catch (AggregateException ae)
            {
                ae.Handle(ex =>
                {
                    if (ex is IndexOutOfRangeException)
                        Console.WriteLine(ex.Message);
                    return ex is InvalidOperationException;
                });
            }
        }

In the code example given above, the IndexOutOfRangeException is handled while the InvalidOperationException is ignored.

Finally, here is how you can call the ExceptionInAsyncCodeDemo() method from the Main method of the Program.cs file.

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

You can take advantage of asynchronous programming to build applications that are scalable and responsive. However, when you use asynchronous methods, keep in mind that the error handling semantics of those methods are different from those of a synchronous method.

Copyright © 2019 IDG Communications, Inc.