C# Corner

Exceptional Async Handling with Visual Studio Async CTP 3

Handling exceptions in the Task-Based Asynchronous Pattern has become much easier with the latest version of the Microsoft .NET Framework.

With the release of the Async Community Technology Preview (CTP), many developers are starting to learn about Microsoft's improvements to the Microsoft .NET Framework for asynchronous programming. In this article, I'll look at how to deal with exceptions that can occur in asynchronous methods. I'll be using the Visual Studio Async CTP version 3 (available for download here).

Putting Things into Context
For sample code such as what I'll be showing in this article, a simple Console application with a few Console.WriteLine commands will suffice. However, if I were to punch out a quick sample of some async code that throws an exception I would be in for a surprise.

If I run this code, nothing prints in the console window. If I set a breakpoint inside the catch block, I never hit it. Confused? It's all because of the context.

When the Task-Based Asynchronous Pattern (TAP) is used in Windows Forms, Windows Presentation Foundation (WPF) or Web applications, there's already a "context" that the code is running in (such as the message loop UI thread for Windows Forms and WPF applications). When async calls are made in those applications, the awaited code goes off to do its work asynchronously and the async method exits back to its caller. In cases where there's already another operating context, program execution returns to the other context.

Console applications don't have the concept of a context. When the code hits the "await task1" call inside the try block, it will exit back to its caller, which in this case is Main. There's no more code after the call DoSomething, so the application ends without the async method ever finishing.

To alleviate this issue (which you'll see in unit tests as well), Microsoft included an async-compatible context with the Async CTP sample code that can be used for console apps or unit-test apps. In fact, the company created three, but I'm using the general-purpose GeneralThreadAffineContext in all the code samples included in this article.

The GeneralThreadAffineContext and its related supporting classes can be found in the My Documents\Microsoft Visual Studio Async CTP\Samples\(C# Testing) Unit Testing\AsyncTestUtilities folder.

Once I have those classes added to my project, I just need to change my Main method to:

private static void Main(string[] args)
{
  GeneralThreadAffineContext.Run(() => DoSomething());
}

Exceptions in Async Methods
Now that I've got the context issue out of the way, my simple example is throwing and capturing exceptions just like it would in regular, synchronous code. When the "task1" being awaited throws the exception in the other context, it's caught by the .NET Framework and thrown back in the calling context (inside the try/catch block). Now I'm going to look at more complex examples when multiple exceptions may be thrown from different contexts.

Listing 1 shows a contrived example that uses a couple of async calls to get information on how many people reside at a school (kids plus staff). The calls to get the number of kids as well as the staff are async because they could be time consuming (for instance, a database call, a Web service and so on). In the Listing 1 code, note how I purposefully wrote both async tasks to throw an exception: one when I make the call to count kids and one when I make the call to count staff. (If you want, you can comment out the two throw statements in Listing 1 to see the code actually work.)

If this code is run, the only exception caught is the one generated when counting kids. The exception thrown when counting the staff is eaten and never propagated. Why?

The two tasks are awaited with the following code:

  var kids = await getKidsTask;
  var staff = await getStaffTask;

When the await on getKidsTask detects the exception, it will propagate it back up to the caller and the getStaffTask is basically abandoned and forgotten about. Microsoft realizes this scenario of throwing away exceptions is bad, so the company has created a new exception that will wrap multiple exceptions from async methods.

Aggregating Exceptions
To combat the issue described previously, the Task class has a "WaitAll" method that will wait for all tasks to complete. I can replace the two await calls with a single call to WaitAll:

Task.WaitAll(getKidsTask, getStaffTask);

Then I need to modify my return statement because I'll have to pull out the results of these tasks manually (something normally handled by the await keyword):

return getKidsTask.Result + getStaffTask.Result;

But what about those exceptions? Because WaitAll will wait for all of the tasks it was given to complete (or fault), it will keep track of all exceptions thrown from any of those tasks. Once all tasks have been completed, if any of them threw an exception, the WaitAll method will create a new AggregateException and populate its InnerExceptions property with all the exceptions thrown. This allows me to change my catch block to something that can examine the InnerExceptions:

catch (AggregateException aex)
{
  foreach (var ex in aex.InnerExceptions)
  {
    Console.WriteLine("Something went wrong counting {0}", ex.Message);
  }
}

Which exception will be first in the InnerExceptions list? Which will be second? There's no way to tell. In testing, I've noticed that the exceptions in the InnerExceptions property tend to appear in the same order they were thrown, based on the task list passed to WaitAll. However, a specific order is not something I plan on relying on, and you shouldn't, either.

You might be wondering why the WaitAll doesn't simply re-throw the exceptions it caught. The problem with re-throwing a specific exception (such as "throw e") is that you lose your stack trace. This isn't something specific to the Task class; it's basic to the .NET Framework. It's one of the reasons that the Exception class has an InnerException property.

The InnerException property contains details (along with a stack trace) that result in a new exception being thrown. By wrapping all of these exceptions thrown from the waited tasks into an AggregateException, the detailed stack trace information is preserved in each exception.

Aggregating Aggregates
Yuck. It's a horrible title for this section, but it bears mentioning. In the sample code, there were two async calls and each one threw an exception. The WaitAll collected the two exceptions and bundled them into an AggregateException.

What if one of the async calls executes more async calls itself, and those nested async calls throw multiple exceptions? In that case, the nested calls would be combined into an AggregateException, and that AggregateException would be thrown as usual and caught by the original WaitAll and bundled into an AggregateException.

So you see, it's possible for an exception in the InnerExceptions of an AggregateException to be another AggregateException! I've reworked my original example so that the staff counting task makes two more (exception-throwing) tasks and uses a WaitAll on them.

Then I've modified my catch clause to include the exception type along with the exception message:

catch (AggregateException aex)
{
  foreach (var ex in aex.InnerExceptions)
  {
    Console.WriteLine("Something [{1}] went wrong counting {0}", ex.Message, ex.GetType().Name);
  }
}

Running this code should produce the following output:

Something [AggregateException] went wrong counting One or more errors occurred.
Something [CountingException] went wrong counting kids
Number of people at school: 0

The TAP is a huge improvement for making asynchronous programming approachable to everyone. Download the Async CTP and check out all the sample code included. Microsoft is putting even more async power directly into the C# 5 language, so things will only get better!

About the Author

Patrick Steele is a senior .NET developer with Billhighway in Troy, Mich. A recognized expert on the Microsoft .NET Framework, he’s a former Microsoft MVP award winner and a presenter at conferences and user group meetings.

comments powered by Disqus

Featured

Subscribe on YouTube