The Beginner’s Guide to Exception Handling with the TPL

The Beginner’s Guide to Exception Handling with the TPL

By Amir Zuker

The Task Parallel Library (TPL) was released officially along with .NET 4.0 and it’s the new way of implementing and consuming asynchronous work.

This post assumes you have basic experience with the TPL already, if not you can read more about it here.  In this post I will discuss the scenarios that exist in the TPL in regards to exception handling, and there are more than a few.

A task represents an asynchronous unit of work, and as you can see, you only need an Action in order to construct one. Another thing you should keep in mind is that a task has a certain life cycle. Conceptually, a task can be either idle (not started), running, pending, cancelled, faulted, and successfully completed. Each task has a Status property which defines the current state of the task. You can examine this enum to get a better understanding of the task’s life cycle.

Let’s get started then, following are key things in regards to exception handling with the TPL.

Faulted Tasks

There are numerous ways in which a task can turn Faulted. One, which is quite straight forward, is when the task is running an action which fails and throws an exception. Examples of other cases can be failing child tasks and task completion source mechanisms.

A faulted task is basically a task which resulted in an actual error and it carries the exception along with it.
For example, the following line yields a task which becomes faulted:

var task = Task.Run(() => { throw new InvalidOperationException(); });

Okay, so we have a faulted task, now what?

Typically, you should encounter an exception if you do one of the following on a faulted task:

  1. Waiting on the task (e.g. task.Wait())
  2. Accessing the task’s result (task.Result)
  3. Awaiting on the task (await task)

All three statements can throw exceptions, so if you care to react, you should surround it with a try/catch statement. The first two will always propagate an exception of type AggregateException, which is basically a container of multiple exceptions, while the latter will throw the actual exception thrown by the implementation.

What about fire-and-forget semantics? What if I were to launch a task which I don’t need to wait for in any way, but still want to react in case it fails? Actually, you can still use await in this case, but there is another option – explicit task continuations. Basically, you can define continuations on a task that could occur in a specific case, for example -

task.ContinueWith(t => LogError(t.Exception), TaskContinuationOptions.OnlyOnFaulted);

That way, I can handle the task exception if it indeed turned faulted, otherwise, this continuation would not take place. I can also wrap it as an extension method and reuse that behavior. Good, but what happens if the developer forgot to handle a faulted task?
Well, I will cover this in a separate topic further down the post.

Cancelled Tasks

A task can be cancelled using the cooperative model of cancellations, read more about it here. Another possibility is that a task was never supposed to be executed, such as defining a continuation with a certain option that might prevent it from taking place.

As you may expect, the actions in which you should encounter an exception in this case are the same as mentioned in the faulted tasks, i.e.

  1. Waiting on the task
  2. Accessing the task’s result
  3. Awaiting on the task

As stated above, if you perform one of these actions and you care to react to exceptions, you should always surround these with a try/catch statements.

Aggregate Exception

As stated above, an aggregate exception is basically a container of multiple exceptions. This exception can turn up in many cases when using the TPL, such as when waiting on a task or accessing its result.

Apart from being an exception of its own, it provides you with a property InnerExceptions which you can enumerate to process all of the contained exceptions. Furthermore, AggregateException can contain a tree-like structure by including other AggregateExceptions within it. If you need to enumerate all the leaf exceptions, you can use the Flatten() method and enumerate over the InnerExceptions property.

Here is what it looks like in OzCode-enhanced DataTip:

Clicking on the exception icon in OzCode’s DataTip will visualize the exception in the excellent Exceptions Trail viewer, which shows you all the inner exceptions in a clickable breadcrumb control.

One more thing that you might find useful is the ability to filter exceptions in order to handle specific ones. For example, if you wanted to handle MyException, you could do the following:

var aggregateEx = new AggregateException( 
    new InvalidOperationException(), 
    new MyException()); 
  
aggregateEx.Handle( 
    ex => 
    { 
        var specific = ex as MyException; 
  
        if (specific == null) return false; 
  
        //handle the exception 
  
        return true; 
    }); 

In this example, you would handle the specific exceptions you care about, in this case MyException.  If there are any other exceptions in the aggregated exception then a new exception is thrown without the ones you filtered out.

Nested Child Tasks

A task can have attached child tasks, which basically binds them together in their life cycle. A parent task will be completed once its child tasks are completed as well, and intuitively, if a child task is faulted, the parent task would become faulted as well.

The following example shows such relation; the outer task is the parent of the inner task and it will be completed and faulted once the child is:

Task.Factory.StartNew( 
    () => 
    { 
        Task.Factory.StartNew(() => { throw new InvalidOperationException(); }, 
            TaskCreationOptions.AttachedToParent); 
    }); 

Unobserved Exceptions

What happens when a task is faulted and no code has accessed the exception to deal with it?

These exceptions are considered as unobserved, and are treated differently depending on the .NET framework version.
In .NET 4.0 unobserved exceptions are propagated back to the joining thread upon finalizing. This could be problematic of course and cause odd behaviors or crashes.
In .NET 4.5 they changed the default behavior and made it so the exception wouldn’t be propagated as such, so one less thing to worry about here, though keep in mind that you might be losing important information because of this.

Lastly, there’s a useful static event that you can subscribe to TaskScheduler.UnobservedTaskException.
This event will fire on every unobserved exception, so this would be a good place to log such errors. In addition, this provides you with a way to turn the exception into an observed exception so it doesn’t propagate back to the joining thread (this is only relevant for .NET 4.0).

Async ‘void’ Methods

This isn’t strictly related to TPL Exception Handling, but it is certainly worth mentioning.
When you define an async method, you can still choose whether to return void or Task/Task<T>.

If you can, it is considered a best practice to always return tasks instead of using ‘void’. The common cases where you can’t are when working with specific delegates that have an unsuitable signature, such as event handlers.

Async void methods behave a bit differently, specifically when it comes to exception handling.
In case of an exception, since there isn’t an actual task that can become faulted, the exception would be propagated back to joining thread, and again could cause odd behaviors or crashes.

The Bottom Line: Best Practices

  1. If you need to wait, await, or access a task’s result – surround it with a try/catch if you need to react to errors.
  2. Subscribe to the TaskScheduler.UnobservedTaskException to log the exception and perhaps set it as observed if you like.
  3. Use the Flatten method before handling the inner exceptions of an aggregate exception.
  4. Prefer async methods that return tasks over void.

That about sums it up pretty well and I hope it cleared some things for you.


About the author: Amir is a Co-Founder of CodeValue, and has lead the design and development of many complex distributed systems from the ground up, using most current .NET technologies, such as WCF, Metro, WPF, Azure, Silverlight, Prism, Data Services, MEF, LINQ, Parallel Extensions and .NET 4.0.

Amir is a co-author of the new Microsoft Official Curriculum (MOC) of WCF 4.0, and a senior expert in the field with unique understanding of SOA. Furthermore, he released an open-source project called WcfContrib, a rich and powerful infrastructure for working with WCF on both client and service side which is available in CodePlex.

Blog:    http://blogs.microsoft.co.il/blogs/zuker
LinkedIn:    http://www.linkedin.com/in/zuker

Comments powered by Disqus