Thursday, September 21, 2017

Exception handling of Tasks in .NET

Exception Handling rules


Task Parallel Library (TPL) handles exceptions well.

If a task throws an exception E that goes unhandled:

  • task is terminated
  • E is caught, saved as part of an AggregateException AE, and stored in task object's Exception property.
  • AE is re-thrown when one of the following is called: .Wait, .Result, or .WaitAll

Example with no exception handling:

Task<int> t = Task.Factory.StartNew(code);
int r = t.Result;

Simple example with exception handling:


Task<int> t = Task.Factory.StartNew(code);
try { int r = t.Result; }
catch (AggregateException ae) 
{
Console.WriteLine(ae.InnerException.Message);
}

Working with AggregateException graph (preferred way)

The above handling is not really sufficient. When catching the AggregateException it can be an graph of Exceptions connected via the InnerExceptions property. For example, AggregateException.InnerExceptions can contain another AggregateExceptions or Exceptions. We need to look at the leaves of the graph to get the real exceptions of interest. An easy way to do this is to use the Flatten() method;

Task<int> t = Task.Factory.StartNew(code);
try { int r = t.Result; }
catch (AggregateException ae) 
{
ae = ae.Flatten();
foreach (Exception ex in ae.InnerExceptions)
Console.WriteLine(ex.Message);
}


Example of what happens when an exception is NOT "observed"

Task t = Task.Factory.StartNew(() =>
{
int d = 0;
int answer = 100 / d;
}
);

This will cause the application to crash when garbage collection is executed.

Exception handling Design and the need for observing

It is highly recommended that you "observe" all unhandled exceptions so that when the task is garbage-collected the exception will be re-thrown then. Unfortunately, this is not the ideal place to handle the exception.

To observe you can do it by doing one of the following:
  • call .Wait or touch .Result - the exception is re-thrown at this point
  • call Task.WaitAll - the exception(s) are re-thrown when all have finished
  • touch the task's Exception property after the task has completed
  • subscribe to TaskScheduler.UnobservedTaskException which is particularly useful if a third party block of code throws the exception.
NOTE: Task.WaitAny() does NOT observe the exception.

Wait Example

try { t.Wait(); }
catch (AggregateException ae) { ... }

Exception accessed Example

if (t.Exception != null)
{ ... }

Result accessed Example

try { var r = t.Result; }
catch (AggregateException ae) { ... }

Last resort exception handling

If you don't "observe" an exception you can still handle it by subscribing to the TaskScheduler.UnobservedTaskException. 

Use cases for using this method:

  • speculative tasks that you don't cancel and don't really care about the result once you get one result. If one of those tasks throws an exception you don't want it to be thrown when garbage collection takes place.
  • Using a third party library that you don't trust and don't know if it will throw any exceptions.
You only want to subscribe once to the event. Good places to do this are in the application startup code, a static constructor, etc.

How to subscribe:

TaskScheduler.UnobservedTaskException += new EventHandler<UnobservedTaskExceptionEventArgs>(MyErrorHandler);

Example handling the error

static void MyErrorHandler(object sender, UnobservedTaskExcpetionEventArgs e)
{
Console.WriteLine($"Unobserved error:{e.Exception.Message}");
e.SetObserved();
}

Note the call to e.SetObserved(). This is what tells .NET that we have observed the Exception and now it will not be thrown at garbage collection. 

Reference

Content is based on Pluralsight video called Introduction to Async and Parallel Programming in .NET 4 by Dr. Joe Hummel. 

No comments: