Error Handling with Tasks

During these explorations of the TPL we assumed that all is well. But what happens if the task that executes code throws an unhandled exception? This matters of course, because else the process might be in some kind of inconsistent state.

Any synchronous method can produce a result or an exception and the same holds true for tasks.
For error handling the three states of a Task are important:

  • Ran to Completion
    root method of task ended gracefully.
  • Canceled
    method was stopped by virtue of OperationCancelledException (see section Cancellation)
  •  Faulted
    implies that the task ended through an unhandled exception

If a Task ends because of an exception, it does not take the process with it. You can handle the error with the return of the Task.
To handle exceptions the first thing that comes to mind is to use try/catch. The question that raises immediately is where to put the try/catch, at creation or on completion. On creation not going to cut it, because the control is directly returned back when a task is started. So the only reasonable ways are on Task.Wait, Task.Result or Task.WaitAll.

var task = Task.Factory.StartNew(() => SomethingAsync());
catch (Exception e)
     Console.WriteLine($"{nameof(SomethingAsync)} went wrong");

The Exception you get is always an AggregateException, which seems odd because it implies multiple exceptions. This is because of possible task parent/child relations. A parent task is only completed if all child tasks are completed (see section on Task Relationships). To get through to the underlying exceptions: use the InnerExceptions property of aggregate exception.
The best way to do this is to utilize the Flatten method, because each exception in the InnerExceptions could be another AggregateException.

catch (AggregateException errors)
     foreach (var error in errors.Flatten().InnerExceptions)
          Console.WriteLine($"{error.GetType().Name}, {error.Message}");

Yet this is still tedious boilerplate. Therefore the Handle method exists. This comes close to the “traditional” try/catch block. Handle takes a predicate style delegate. So you can determine which exceptions should be rethrown if your predicate returns false.

catch (AggregateException errors)

private static bool MyPredicate(Exception exception)
     Type excType = exception.GetType();
     if (excType is DivideByZeroException)
         return false;
         return true;

Handle would rethrow any DivideByZeroException, and continue gracefully on any other kind of exception.

Fire and Forget and Ignoring errors

What happens if you have fire and forget style methods, where you do not care for the return or wait of the task? Is it safe to ignore those cases? (probably not) Also it depends on .Net framework and the completion status of the task.

.Net 4.0

With .Net 4.0 a failure to inspect the exception would terminate the application. With a reference you have the opportunity, without (as in case of fire and forget) you cannot observe the results.
The awkward thing is, that a fire and forget style method with an exception would end the process on the finalization queue, because it is rethrown there, during the process of garbage collection. This is very problematic because of the potentially high delay that would lead to difficult debugging and probably result in subtle errors etc.

The solution for this is to handle all Unobserved Task exceptions by subscribing to the TaskScheduler.UnobservedTaskException of the Application.

TaskScheduler.UnobservedTaskException += HandleUnobserved;

private static void HandleUnobserved(object sender, UnobservedTaskException e)
    Console.WriteLine(sender is Task);

But Exceptions are best handled close to source, therefore this handler should be used for logging purposes only.

.Net 4.5

The solution for this problem that was introduced with .Net 4.5 was to simply not rethrow on the finalization thread. If you have no handler registered for UnobservedTaskException, TPL simply swallows the error…

I do not like this style…Exceptions are there for a reason. So make sure to handle exceptions in your fire/forget mehtods.

Categories: TPL


Leave a Reply