TPL and Error Handling

As of .NET 4.0, the TPL or Task Parallel Library is king when it comes to parallelization. It allows for smooth, easy multi-threading for any application. There is a slight learning curve, however, and a major part of this is understanding how Exceptions bubble-up while using the TPL.

Let’s partake in a simple example. This code will create and run a task that throws an Exception, and then attempt to catch it:

static void Main(string[] args)
{
    try
    {
        // Initialize a Task which throws exception
        var task = new Task(() =>
        {
            throw new Exception("Task broke!");
        });
        // Start the Task
        task.Start();
    }
    // Attempt to catch the Task Exception
    catch (Exception ex)
    {
        Console.WriteLine("Caught the TPL exception");
    }

    Console.WriteLine("End of method reached");
    Console.ReadKey();
}

When we run it, we get this result:

/img/tpl-and-error-handling-first-output.png

The Output

The Exception was not caught. This is because the Task is run on a different thread, which has its own Stack memory and execution path, which consists of the code inside of the Task and not much else. As a result, when the Task is run, the flow of control returns to the next line in our application, unaware of the Task or its outcome. Once the Exception in the Task is thrown, it is thrown in a different scope, effectively outside of our Main method. It’s almost as if we ran an entirely different application, but attempted to catch its Exception in this application. This just won’t work.

There is a way to work around this however. One interesting thing about the TPL is the Task.WaitAll method. It allows the control of flow of your executing Tasks/threads to return to your calling method. By adding this method to our code, we can make our main thread stall until the Task completes, which also enables it to catch the Task’s exception:

static void Main(string[] args)
{
    try
    {
        // Initialize a Task which throws exception
        var task = new Task(() =>
        {
            throw new Exception("Task broke!");
        });
        // Start the Task
        task.Start();
        // Wait for the Task to complete, thus keeping control of flow
        Task.WaitAll(task);
    }
    // Attempt to catch the Task Exception
    catch (Exception ex)
    {
        Console.WriteLine("Caught the TPL exception");
    }

    Console.WriteLine("End of method reached");
    Console.ReadKey();
}

The output is as follows:

/img/tpl-and-error-handling-second-output.png

Second Output

This time we were able to catch the Exception. Of note, the Exception which is thrown by Tasks is typically the AggregateException which allows you to catch multiple, often asynchronous Exceptions as one Exception (which is easier for a single thread to handle). A quick demo of this functionality (let’s have 5 threads each throw an Exception):

static void Main(string[] args)
{
    try
    {
        // A List of Tasks
        var taskList = new List<Task>(5);

        for (int i = 0; i < 5; i++)
        {
            // Initialize a Task which throws exception
            var task = new Task(() =>
            {
                throw new Exception("It broke!");
            });
            // Start the Task
            task.Start();
            // Add to List
            taskList.Add(task);
        }
        // Wait for all Tasks to complete, thus keeping control of flow
        Task.WaitAll(taskList.ToArray());
    }
    // Attempt to catch the Task Exception
    catch (AggregateException agex)
    {
        Console.WriteLine("Caught the TPL exception(s)");
        // Output all actual Exceptions
        foreach (var ex in agex.InnerExceptions)
        {
            Console.WriteLine(ex.Message);
        }
    }

    Console.WriteLine("End of method reached");
    Console.ReadKey();
}

And the output:

/img/tpl-and-error-handling-third-output.png

Third Output

So as you can see, there are ways to handle Tasks throwing Exceptions, if you have your calling thread pause and wait for the Tasks to complete. This, of course, is not always practical so your other option is to handle the exception within the Task’s scope – just like you would with regular, single-threaded code. In effect you treat the code within the Task as isolated, single-threaded code, and catch and handle Exceptions accordingly. A slight mod to our application will show you this:

static void Main(string[] args)
{
    for (int i = 0; i < 5; i++)
    {
        // Initialize a Task which throws exception
        var task = new Task(() =>
        {
            try
            {
                throw new Exception("It broke!");
            }
            catch (Exception ex)
            {
                // Output the Exception
                Console.WriteLine(ex.Message);
            }
        });
        // Start the Task
        task.Start();
        // Note: no longer waiting for Task to finish
    }

    Console.WriteLine("End of method reached");
    Console.ReadKey();
}

And the result of this change:

/img/tpl-and-error-handling-fourth-output.png

Fourth Output

Note the interesting results of this. Our main thread did not wait for the Tasks to complete, and so it the Task results were never returned back to the calling thread’s flow of control. As a result, the “End of method reached” text actually came before the threads crashed, because it happened sooner.

Note also that only 4 of the 5 Exceptions were written to the Console. This is due to a race condition in your parallelized application: The Console is a single/static reference and so our 5 spawned threads plus 1 main thread all race to output to it. In this particular instance of the application being run, the Console.ReadKey method was executed after the first 4 Tasks had written their output, but before the 5th Task wrote its output. This does not mean that it was not handled; it simply means that, in a way that is classic to multi-threaded applications, we encountered a race condition. I ran this application another 10 or 20 times and saw many variations: sometimes 3 Exceptions were output, sometimes 4, and rarely all 5. This is a great example of a race condition within an application, and one which you could handle using the above strategy of Task.WaitAll prior to outputting your final Console.ReadKey statement and terminating your application.

It’s up to you individually to decide which style of TPL error handling makes the most sense in each application. It depends on the purpose of each thread and the implications of having your application wait for threads to complete, considered on a per-application basis.

One final note is that other parallel operations, such as Parallel.ForEach and Parallel LINQ (PLINQ) use AggregateException as well to catch their thrown Exceptions, and that the AggregateException offers a Flatten method for re-throwing nested AggregateExceptions as a one-level deep single AggregateException to simplify handling of them.


See also