Handling Exceptions

There are no special features for propagating exceptions through a continuation chain. Any exceptions thrown by any Task in a continuation chain must be processed, or they will be treated as unhandled exceptions when the finalizer for the Task is performed. See the previous chapter for details of processing Task exceptions. Listing 4-8 illustrates the problem with exceptions in chains.

Listing 4-8. Unhandled Exceptions in Continuation Chains using System;

using System.Threading.Tasks; namespace Listing_08 { class Listing_08 {

cancel token");

to complete finish");

static void Main(string[] args) {

// create a first generation task Task gen1 = new Task(() => { // write out a message

Console.WriteLine("First generation task");

// create a second-generation task Task gen2 = gen1.ContinueWith(antecedent => { // write out a message

Console.WriteLine("Second generation task - throws exception"); throw new Exception();

// create a third-generation task Task gen3 = gen2.ContinueWith(antecedent => { // write out a message

Console.WriteLine("Third generation task");

// start the first gen task gen1.Start();

// wait for the last task in the chain to complete gen3.Wait();

// wait for input before exiting Console.WriteLine("Press enter to finish"); Console.ReadLine();

The second-generation Task throws an exception, but the third-generation Task still runs. The main thread waits for the last Task in the chain to complete and prompts the user to press the Enter key. The program then exits, causing the finalizer to be called, at which point the exception from the second-generation continuation is propagated, producing the following output:

First generation task

Second generation task - throws exception

Third generation task

Press enter to finish

Unhandled Exception: System.AggregateException: A Task's exception(s) were not observed either by Waiting on the Task or accessing its Exception property. As a result, the unobserved exception was rethrown by the finalizer thread. —> System.Exception: Exception of type 'System.Exception' was thrown. at Listing_08.Listing_08.<Main>b_1(Task antecedent)

at System.Threading.Tasks.Task.<>c__DisplayClassb.<ContinueWith>b__a(Object obj) at System.Threading.Tasks.Task.InnerInvoke() at System.Threading.Tasks.Task.Execute() — End of inner exception stack trace — at System.Threading.Tasks.TaskExceptionHolder.Finalize()

The best way to handle this kind of problem is to have each continuation Task check the status of the antecedent and handle the exception. You can rethrow the same exception to propagate it throughout the continuation chain. Doing so will cause all of the tasks in a chain to be scheduled and executed, but it does reduce the chance of an unhandled exception. Listing 4-9 demonstrates this check-and-propagate approach.

Listing 4-9. Propagating Exceptions Along a Continuation Chain using System;

using System.Threading.Tasks; namespace Listing_09 { class Listing_09 {

static void Main(string[] args) {

// create a first generation task Task gen1 = new Task(() => { // write out a message

Console.WriteLine("First generation task");

// create a second-generation task Task gen2 = gen1.ContinueWith(antecedent => { // write out a message

Console.WriteLine("Second generation task - throws exception"); throw new Exception();

// create a third-generation task

Task gen3 = gen2.ContinueWith(antecedent => {

// check to see if the antecedent threw an exception if (antecedent.Status == TaskStatus.Faulted) { // get and rethrow the antecedent exception throw antecedent.Exception.InnerException;

// write out a message

Console.WriteLine("Third generation task");

// start the first gen task gen1.Start();

// wait for the last task in the chain to complete gen3.Wait(); } catch (AggregateException ex) { ex.Handle(inner => {

Console.WriteLine("Handled exception of type: {o}", inner.GetType()); return true;

// wait for input before exiting Console.WriteLine("Press enter to finish"); Console.ReadLine();

By having the continuation process the exceptions thrown by the antecedent, we can catch exceptions thrown by the last Task in the chain and be sure to avoid unhandled exceptions. Note that the Exception property of an antecedent returns an instance of AggregateException. The InnerException property is read in the continuation Task to avoid nested instances of AggregateException, unpacking the nesting in the exception handler would also work. See the previous chapter for details of how to process instances of AggregateException.

The same issue exists when performing multitask continuations. If any of the antecedent Tasks have thrown an exception that you don't process, that exception becomes unhandled and will cause problems later. Processing antecedent exceptions with the ContinueWhenAll() method is simply a matter of checking each antecedent, such as with the following fragment:

Task.Factory.ContinueWhenAll(tasks, antecedents => { foreach (Task t in antecedents) {

if (t.Status == TaskStatus.Faulted) { // ...process or propagate...

Handling exceptions when using the ContinueWhenAny() method is more difficult. The continuation has one antecedent, but one of the other Tasks from the previous generation may throw an exception and this might well happen after the continuation has been executed. The best way to avoid unhandled exceptions in this situation is to combine a selective ContinueWhenAny() continuation with a ContinueWhenAll() that exists purely to process exceptions, as in the following fragment:

Task.Factory.ContinueWhenAny(tasks, antecedent => {

// ...task continuaton code... }, TaskContinuationOptions.NotOnFaulted);

Task.Factory.ContinueWhenAll(tasks, antecedents => { foreach (Task t in antecedents) {

if (t.Status == TaskStatus.Faulted) { // ...process exceptions...

0 0

Post a comment