Saturday, September 01, 2012

Task ContinueWhenAll and Failures

Lately I've been using Task.Factory.ContinueWhenAll in my TPL programming, but I wasn't quite sure how it handles error nor the cleanest way to handle errors coming from inner tasks.

Take a look at the code below for an example. What do you expect the result to be?
Task innerTask1 = Task.Factory.StartNew(() => { throw new Exception("Error1"); });
Task innerTask2 = Task.Factory.StartNew(() => { throw new Exception("Error2"); });

var task = Task.Factory.ContinueWhenAll(
    new[] { innerTask1, innerTask2 },
    innerTasks =>
        {
            foreach (var innerTask in innerTasks)
            {
                Console.WriteLine("Result: {0}", innerTask.Status);
            }
        });
var exceptionTask = task.ContinueWith(
    t => Console.Error.WriteLine("Outer Task exception: {0}", t.Exception),
    TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously);

try
{
     task.Wait();
}
catch (Exception e)
{
     Console.WriteLine(e);
}

Console.WriteLine("Done - Outer Task Status: {0}, Exception Task: {1}", task.Status, exceptionTask.Status);
Console.ReadLine();
After running this code, you will see:
Result: Faulted
Result: Faulted
Done - Outer Task Status: RanToCompletion, Exception Task: Canceled
So both inner tasks finished with status "Faulted", as indicated in the "Result:" lines, but the outer task finishes as "RanToCompletion" (i.e. successful). At some point later the unobserved exceptions will be thrown in the code when finalized, which is far from ideal if you want to have a good understanding of this code and be able to debug any issues in the future.

In order to properly get the "task" up there to fail if any of the inner tasks fail is to observe the exceptions when ContinueWhenAll continuation runs. One of the most obvious is to check all innerTasks and, if any failed, we can throw its exception.

Today I came across a good on that. The suggest is to make the following change to the code:
Task innerTask1 = Task.Factory.StartNew(() => { throw new Exception("Error1"); });
Task innerTask2 = Task.Factory.StartNew(() => { throw new Exception("Error2"); });

var task = Task.Factory.ContinueWhenAll(
    new[] { innerTask1, innerTask2 },
    innerTasks =>
        {
            Task.WaitAll(innerTasks);

            foreach (var innerTask in innerTasks)
            {
                Console.WriteLine("Result: {0}", innerTask.Status);
            }
        });
var exceptionTask = task.ContinueWith(
    t => Console.Error.WriteLine("Outer Task exception: {0}", t.Exception),
    TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously);

try
{
     task.Wait();
}
catch (Exception e)
{
     Console.WriteLine(e);
}

Console.WriteLine("Done - Outer Task Status: {0}, Exception Task: {1}", task.Status, exceptionTask.Status);
Console.ReadLine();
The difference is the "Task.WaitAll" - that makes the exceptions to be observable and the ContinueWhenAll task to fail, thus making the code much more predictable and understandable in case of failures.

This is the result as compared to the output above (just the "Done" line, as I don't want to past the WriteLine that prints the exception here):
Done - Outer Task Status: Faulted, Exception Task: RanToCompletion
So the tip is: for all ContinueWhenAll, add Task.WaitAll. If you have an empty continuation (_ => {}) or any variation of that), just replace it with "Task.WaitAll" - and here it can be a method group, which is even cleaner.
Post a Comment