9

Using TPL/Tasks I can perform exception handling using inner try/catch statement:

  Task.Factory.StartNew(
    ()=>
      {
        try
        {
          // Do stuff
        }
        catch
        {
          // Handle exception
        }
      });

Or using ContinueWith, like so:

Task.Factory.StartNew(
    ()=>
      {
          // Do stuff
      }).ContinueWith(
        task =>
        {
          if(task.Exception != null)
            // Handle exception
        });

Which method is more recommended to use? What are the cons and pros for each?

Eran Betzalel
  • 4,105
  • 3
  • 38
  • 66

5 Answers5

3

It's largely down to what your design needs. Some things to consider:

Catch exceptions in the tasks that throw them

  • When a task represents some indivisible unit of work that includes cleaning up after particular exception types.
  • When a particular exception type shouldn't propagate outside the task for some reason, e.g. it needs to be wrapped in an outer exception of a different type to meet client code's expectations of the contract.

Handle exceptions in continuations

  • When the exception cleanup should be scheduled by a different TaskScheduler, e.g. running "primary" tasks on the thread pool but marshalling all exception logging onto a UI thread.
  • If it makes sense to have multiple continuations each do different things with the exception, though that would be slightly unusual.
  • To ensure that exceptions from Tasks whose code you don't supply are observed and handled appropriately, e.g. properly cleaning up after tasks created by TaskFactory.FromAsync. Though depending on circumstances, that could as well be done by waiting on the Task.
anton.burger
  • 5,637
  • 32
  • 48
3

If you are able to properly handle the exception within the method the task itself is throwing you should be catching it within that first task, and not in a continuation, unless you have some compelling reason not to do so. Creating a continuation at the same scope as the task itself (as is done in your second example) is needlessly adding more work.

It's either useful or necessary to handle exceptions in continuations when the exception is handled from an entirely different scope than where the task is defined. For example, if you have a method that is given some arbitrary task, and it has no idea what the definition of that task might be, but it needs to do something in the event that the code threw an exception you'll need to have a continuation that handles the exception.

Note that if you are going to have a continuation that handles an exception you can use TaskContinuationOptions.OnlyOnFaulted to only run the continuation when the task throws an exception, rather than doing the check within the definition of the continuation.

Servy
  • 202,030
  • 26
  • 332
  • 449
1

To some extent, it's a matter of preference, especially if you 'own' the task code and the calling code. Here are some things to consider.

Firstly, you should only catch exceptions that you know how to handle. This applies whether you handle them with a continuation or with a try/catch inside the action.

Note also the changed behaviour in .NET 4.5 regarding uncaught exceptions. The change is effecitvely from the 'purist' approach (tear down the process on an uncaught task exception) to a less draconian one. Still, it's not good deliberately to rely on the new behaviour.

As for which of your two alternatives to favour, there is an argument for choosing the second one: handling the exception in a continuation. It will be increasingly common in .NET for methods to return a Task. For example, Stream.ReadAsync. To use such methods correctly you need a continuation (either the conventional way, or using a try/catch block with the new await feature, which amounts to the same thing, but is much easier to code and read). So it's good to get into the habit of assuming that any Task may fail unless you explicitly know otherwise, and coding appropriate exception handling behavior.

In case you're interested, here is an alternative way of coding your second example in .NET 4.5.

async Task MyMethod()
{
    try
    {
        await Task.Run(
            () =>
            {
                // Some work.
            });
    }
    catch (SomeException ex)
    {
    }
}

Another difference most frequently applies in Windows Forms or WPF applications where your code is called from the UI thread. Here the default behaviour of the TPL when using await is to run continuations using a synchronization context that marshals them back to the UI thread. That is, if your Task.Run is called from the UI thread, the continuation will also run on the UI thread.

This is useful if you want to display a dialog to the user in response to an exception. You would not be able to do that successfully from within the Task worload. When using explicit continutations rather than await, you must pass a TaskScheduler created using TaskScheduler.FromCurrentSynchronizationContext to the appropriate overload of ContinueWith.

Community
  • 1
  • 1
Olly
  • 5,966
  • 31
  • 60
  • “it's good to get into the habit of assuming that any Task may fail” Does that also include the `Task` returned from `ContinueWith()`? – svick Feb 13 '13 at 16:30
  • 1
    Also, you're wrong, normal continuation (using `ContinueWith()`) won't run back on the UI thread, unless you explicitly specify it. – svick Feb 13 '13 at 16:32
  • @svick - Thanks. Clarified and corrected as per your comments. Interestingly, `.ContinueWith` uses the *current* `TaskScheduler`, which is certainly confusing - well, it evidently confused me anyway ;-) – Olly Feb 13 '13 at 17:07
1

Your two examples are conceptually different.

The first handles your exception internally inside the executing task. Any code running after the catch would still be executed.

The second schedules another async task that will always be run by the scheduler after the first task has finished.

Guess the answer is it depends on exactly what you are trying to achieve - there is no clear cut answer - but the second is more in keeping with the tpl.

Also, in the second example, task.IsFaulted is clearer that task.Exception

Matt Randle
  • 1,885
  • 2
  • 17
  • 22
-1

I'd say that it depends on the context. as Olly said, you should only handle exceptions that you know how to handle. I'd go to say you should handle the exception if you know how to handle it.

An example would be if you have a task that should load some data from file or fall back to some default value (which can throw exception) one way to do that would be (Pseudo code):

Task.Factory.StartNew(()=>
{
    MyObject objectToSet = null;
    try
    {
        objectToSet = File.Open("mydata");
    }
    catch (FileException ex)
    {
        // this will catch the FileException because we know how to handle that!

        // the following will however throw an exception that we cannot handle
        objectToSet = GetFallBackValue(); 
    }
    // when we are here we promise that the objectToSet is valid.
});

In the case of the File.Open, we know how to continue. In the case of the GetFallBackValue() we don't, so we propagate it to the caller, stating that we are in an inconsistent state.

Community
  • 1
  • 1
default
  • 11,485
  • 9
  • 66
  • 102
  • 1
    Thanks for the information, but what does that has to do with my question? – Eran Betzalel Feb 13 '13 at 16:02
  • your question was "which one is more recommended" and I said "it depends on the context" and explained with an example. what question are you referring to? – default Feb 14 '13 at 07:35
  • So, in your example, you'll also recommend to use ContinueWith as a safety measure for `GetFallBackValue()`? – Eran Betzalel Feb 15 '13 at 09:05
  • @EranBetzalel exactly. it should be documented for the method though, if it can throw exceptions. it's the same thing with the `ContinueWith` really, you should catch the exceptions that you can handle. Capturing a general `Exception` is never a good idea really, (unless you do something like `log(e); throw;` just to log it and pass it on), because it might be an exception that you are unable to recover from (for instance, something like `OutOfMemoryException`). So it's not so much a safety measure as it is a "I know how to recover from this exception` handling. – default Feb 15 '13 at 12:06