locked
Exception handling best practice in tasks RRS feed

  • Question

  •  

    Hi,

    since there are many ways to handle exceptions thrown in tasks I would like to now what's the best practice.

    Exceptions could be handled by:
    1. using try-catch within the task method
    2. observing the task's exceptions using Wait, WaitAll, reading the Exception property (in a continuation that runs when the task ends in a faulty state or after waiting using a WaitHandle or CancellationToken), using a Task<TResult> and reading the Result property or implicitely in a child task
    3. the TaskScheduler.UnobservedTaskException event (global for the app domain)

    I'm not sure when or even if to use the first approach. I think this could be used if exceptions could be handled by the task itself. Another usage scenario would be fire and forget tasks (you could use a continuation that ignores exceptions as well for this).

    The second approach is perfect for many cases, but since observing does not work when using Wait or WaitAll with a timeout, waiting with a WaitHandle or a CancellationToken (and some other scenarios I forgot here) you have to be aware of it (and even document this behaviour in the code). For the waiting cases that are not observed we should read Exception after the wait or include a continuation.

    The third approach is in my mind absolutely nessecary for the cases where Exceptions in tasks are not handled by the application, possibly because we simply forgot to handle them.

    Is that right?

    If using the first approach, should we catch and rethrow ThreadAbortException and AppDomainUnloadedException? Or should we just ignore them?

    Sunday, April 25, 2010 7:55 AM

Answers

  • If you can handle the exception within the task's body and it makes sense to do so, you should.  For example, if you can recover from the exception and still produce a useful outcome, it's best to do that within the task such that when the task ends in the RanToCompletion state, the code consuming the task can rest assured that the work meant to be performed by the task has completed.  However, your application may be constructed in a way where this isn't feasible.  For example, your Task<TResult> may incur an exception and not be able to produce a valid TResult; as such, handling the exception within the task isn't going to be helpful, as you'd have to return some TResult, making the consuming code think that the returned value is the valid result. (You could work up your own scheme whereby a certain TResult is deemed invalid, but that complicates things further.)  Here, it's best to let the exception go unhandled within the task.  If the task knows it's going to be fire-and-forget, which by definition means it's a Task and not a Task<TResult> (since if it's fire and forget no one would be using the result), then it might make sense still to try/catch the specific exceptions you expect, log them, etc., assuming the app is safe to continue running even after those exceptions have happened.

    Code that consumes tasks needs to assume that they might end in the Faulted state. As such, code needs to be prepared to deal with any exceptions that might have been generated by such tasks.  For continuations off of the tasks, those continuations won't be scheduled until the antecedent task(s) has completed, in which case the exception information will already be available.  Same goes for code waiting on one or more tasks until they complete; the exception information will be available by the time the wait completes.  Of course, as you point out, there are ways to short-circuit this.  You can bail out of waits early, for example, in which case you're  bailing before the exception information is available.  For these cases, you still want to observe the exception information, as it could convey something really bad.  What you do at this point depends on the needs of your application, but continuations are valuable for being notified of exceptions when they occur (you can even register continuations to run OnlyOnFaulted).

    UnhandledTaskException is really a stop-gap measure.  It's useful in extreme cases, but from a design point of view, it's better to handle the exceptions as close to their source as possible.

    Sunday, April 25, 2010 6:47 PM
    Moderator

All replies

  • If you can handle the exception within the task's body and it makes sense to do so, you should.  For example, if you can recover from the exception and still produce a useful outcome, it's best to do that within the task such that when the task ends in the RanToCompletion state, the code consuming the task can rest assured that the work meant to be performed by the task has completed.  However, your application may be constructed in a way where this isn't feasible.  For example, your Task<TResult> may incur an exception and not be able to produce a valid TResult; as such, handling the exception within the task isn't going to be helpful, as you'd have to return some TResult, making the consuming code think that the returned value is the valid result. (You could work up your own scheme whereby a certain TResult is deemed invalid, but that complicates things further.)  Here, it's best to let the exception go unhandled within the task.  If the task knows it's going to be fire-and-forget, which by definition means it's a Task and not a Task<TResult> (since if it's fire and forget no one would be using the result), then it might make sense still to try/catch the specific exceptions you expect, log them, etc., assuming the app is safe to continue running even after those exceptions have happened.

    Code that consumes tasks needs to assume that they might end in the Faulted state. As such, code needs to be prepared to deal with any exceptions that might have been generated by such tasks.  For continuations off of the tasks, those continuations won't be scheduled until the antecedent task(s) has completed, in which case the exception information will already be available.  Same goes for code waiting on one or more tasks until they complete; the exception information will be available by the time the wait completes.  Of course, as you point out, there are ways to short-circuit this.  You can bail out of waits early, for example, in which case you're  bailing before the exception information is available.  For these cases, you still want to observe the exception information, as it could convey something really bad.  What you do at this point depends on the needs of your application, but continuations are valuable for being notified of exceptions when they occur (you can even register continuations to run OnlyOnFaulted).

    UnhandledTaskException is really a stop-gap measure.  It's useful in extreme cases, but from a design point of view, it's better to handle the exceptions as close to their source as possible.

    Sunday, April 25, 2010 6:47 PM
    Moderator
  • Thank's again, Stephen, for this great explanation. I ow you one :-)
    Sunday, April 25, 2010 7:37 PM