Handling exceptions in tasks

This article illustrates the different scenarios and techniques of exception handling when working with tasks.

For any multithreaded programming paradigm, an important aspect to consider is exception handling. It’s not as straight forward as in case of single threaded programs. This is because, not all cases of multithreaded programming report exceptions back to the calling thread. Task parallel library is no exception. However, in this case, we have a few classes and constructs that allow us to handle such scenarios better.

When exceptions are propagated back to the calling thread?
Exceptions get propagated to the calling thread when it waits on the spawned tasks by using static/instance versions of Wait methods defined by Task or Task<TResult> classes. Such calls should be wrapped in a try-catch block. A single task can be a parent of attached child tasks or the calling thread might wait on multiple tasks. To propagate all exceptions back to the calling thread, the Task library uses AggregateException to wrap all the exceptions. It does so, even if there is a single exception thrown. The AggregateException class contains an InnerExceptions property which contains the original exceptions that were thrown.

Example:

using System;
using System.Linq;
using System.Threading.Tasks;

namespace Tasks.Exceptions
{
class Program
{
static void Main(string[] args)
{
try
{
var t1 = Task.Factory.StartNew(() => { throw new Exception("Thrown by task1"); });
var t2 = Task.Factory.StartNew(() => { throw new Exception("Thrown by task2"); });
Task.WaitAll(t1, t2);
}
catch (AggregateException ex)
{
ex.InnerExceptions.ToList().ForEach(e =>
Console.WriteLine("handled exception with message: {0}", e.Message));
}
}
}
}

In this example, we spawn two tasks (which doesn’t do anything other than throwing an exception, just for demonstration purposes). The main thread waits for both of them using the static WaitAll method defined on the Task class. This call is wrapped in a try-catch block that catches an AggregateException. The handling logic just loops through the InnerExceptions property of the AggregateException class and prints the message of each wrapped exception (again, for demonstration and we can have any custom handling logic here).
Try removing the call to Task.WaitAll and check if you still get the exception messages printed to the console.

Exceptions thrown by attached child tasks:
The user code running in a task can create further tasks. Such tasks are called child tasks of the parent (from where they were spawned). But unlike children in real world, such tasks are detached from parent by default. To create them as attached tasks, we can use an overload of StartNew method of Task.Factory that takes in an instance of the enumeration TaskCreationOptions and pass its value as AttachedToParent.
Here’s how this can be done:

using System;
using System.Linq;
using System.Threading.Tasks;

namespace Tasks.Exceptions
{
class Program
{
static void Main(string[] args)
{
try
{
var grandParent = Task.Factory.StartNew
(() =>
{
var child = Task.Factory.StartNew(() =>
{
var grandchild = Task.Factory.StartNew(() =>
{
throw new Exception("Exception thrown by grandchild");
}, TaskCreationOptions.AttachedToParent);

throw new Exception("Exception thrown by child");
}, TaskCreationOptions.AttachedToParent);

throw new Exception("Exception thrown by grandparent");
});

grandParent.Wait();
}
catch (AggregateException ex)
{
ex.Flatten().InnerExceptions.ToList().ForEach(e =>
Console.WriteLine("handled exception with message: {0}", e.Message));
}
}
}
}

As you can see here, we create nested levels of attached child tasks passing a value of TaskCreationOptions.AttachedToParent when we call StartNew. Finally, the main thread waits on the outermost task. For exceptions thrown by an attached child, the task library wraps them into an AggregateException instance before propagating to the parent, which does the same thing while propagating exceptions up the hierarchy.
So, for such cases, when the code in the calling thread catches an AggregateException instance, it’s InnerExceptions property may not always contain the actual exceptions thrown but can in turn (and in this example, it will) contain instances of AggregateException. To access the original exceptions we can code our catch block navigate the exception hierarchy using InnerExceptions property of nested AggregateException instances, but there is an easier way.
The AggregateException class provides a Flatten method, which returns an AggregateException instance with all original exceptions populated in its InnerExceptions property (our example here uses that method).

Exceptions thrown by detached child tasks:
As we said, by default, child tasks are detached from their parent. Exceptions thrown by such children, are not propagated to the calling thread as the earlier case, but have to be handled by the immediate parent. The parent can then rethrow the exception (if we follow this approach for all parents, then the exceptions rethrown by the outermost parent get wrapped in an AggregateException instance before being propagated to the calling thread.

Example:

using System;
using System.Linq;
using System.Threading.Tasks;

namespace Tasks.Exceptions
{
class Program
{
static void Main(string[] args)
{
try
{
var task = Task.Factory.StartNew(() =>
{
var child = Task.Factory.StartNew(() =>
{
throw new Exception("Exception thrown by child");
});
child.Wait();
});

task.Wait();
}
catch (AggregateException ex)
{
ex.Flatten().InnerExceptions.ToList().ForEach(e =>
{
Console.WriteLine("handled exception with message: {0}", e.Message);
});
}
}
}
}

Here, the parent task explicitly waits for the child task and we get to see the exception message in the console output. If we remove the wait on the child task, we cannot see the exception message anymore (since it won’t be propagated).

Exceptions in Cancellation:
Tasks support cancellation. The StartNew method has overloads that accept a CancellationToken as an additional argument. To respond gracefully to a cancellation request, the user code running in a task should throw an OperationCanceledException with the cancellation token (on which the request was communicated) as an argument. Before propagating the exception, the task instance compares this token with the one that was passed to it during its creation. If they are same, then it propagates a TaskCanceledException wrapped in an AggregateException.
However, it doesn’t propagate if the calling thread doesn’t wait on the task.

Example:

using System;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;

namespace Tasks.Exceptions
{
class Program
{
static void Main(string[] args)
{
try
{
var cts = new CancellationTokenSource();
var token = cts.Token;
var task = Task.Factory.StartNew(() =>
{
var ct = token;
while (true)
{
//Silumate work...
Thread.SpinWait(50000);
//This is a concise way to throw an OperationCanceledException with the
//token as an argument
ct.ThrowIfCancellationRequested();
}
}, token);

//This triggers the cancellation
cts.Cancel();

task.Wait();
}
catch (AggregateException ex)
{
ex.InnerExceptions.ToList().ForEach(e =>
{
Console.WriteLine("handled exception of type: {0} with message: {1}", e.GetType().Name,
e.Message);
});
}
}
}
}

Filtering inner exceptions:
When there are multiple inner exceptions within an instance of AggregateException, one might want to handle specific types of exceptions (may be excepted ones) and propagate the rest as-is. While we can do it by looping through the inner exceptions and checking the type of each to take specific action, there’s a neater way to do this using the Handle method of the AggregateException class. This takes in a Func<Exception, bool> delegate instance which gets called for each inner exception. The code inside the delegate can then check the exception type and handle it as per the slated logic. It should return true if it handles the exception, otherwise, false.

Example:

using System;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;

namespace Tasks.Exceptions
{
class Program
{
static void Main(string[] args)
{
try
{
var task1 = Task.Factory.StartNew(() =>
{
throw new ArgumentException("Arg exception thrown by task1");
});
var task2 = Task.Factory.StartNew(() =>
{
throw new NotImplementedException("Not implemented exception thrown by task2");
});

Task.WaitAll(task1, task2);
}
catch (AggregateException ex)
{
ex.Handle(e =>
{
if (e is ArgumentException)
{
Console.WriteLine("Handled {0}", e.Message);
return true;
}

return false;
});
}
}
}
}

The code here spawns two tasks, one of which throws an ArgumentException and the other throws a NotImplementedException. The logic in the Hanlde method returns true for an ArgumentException (to indicate it is handled). If we run this program in debug mode, we will see the error message for task1 in console output, but the program still will terminate since the other exception from task2 goes unhandled.

Accessing Task.Exception property:
If a task completes in a Faulted state, one can examine its Exception property to look at the exception that caused it. A good way to do that is to create a continuation task that runs only if the parent terminates in a faulted state.

Example:

var task1 = Task.Factory.StartNew(() =>
{
throw new ArgumentException("Arg exception thrown by task1");
}).ContinueWith((t) =>
{
Console.WriteLine("Got an exception of type: {0}",
t.Exception.InnerException.GetType().Name);
}, TaskContinuationOptions.OnlyOnFaulted);

task1.Wait();

By Indranil Chatterjee   Popularity  (2155 Views)