Tasks and Unhandled Exceptions

Tasks and Unhandled Exceptions

  • Comments 14

Prior to the .NET Framework 2.0, unhandled exceptions were largely ignored by the runtime.  For example, if a work item queued to the ThreadPool threw an exception that went unhandled by that work item, the ThreadPool would eat that exception and continue on its merry way.  Similarly, if a finalizer running on the finalizer thread threw an exception, the system would eat the exception and continue on executing other finalizers.

That changed for .NET 2.0.  Exceptions are meant to indicate a problem, and automatically eating unhandled exceptions often hides significant errors and reliability problems in an application.  As such, for .NET 2.0, this unhandled exception behavior was changed by default to more closely match how exceptions are handled by Windows in general: if an exception goes unhandled, the process comes crashing down.  While this may seem harsh, allowing the process to crash typically makes it easier to catch an underlying problem, but more importantly, it prevents the application from continuing to hobble along in a potentially very bad state, with corrupted data, etc.  Thus, since .NET 2.0, if a work item running on the ThreadPool throws an unhandled exception, by default the process crashes.

There’s a notable exemption to this behavior, even after 2.0: the Asynchronous Programming Model (APM) pattern.  With the APM pattern, work is started asynchronously with a BeginXx method, and at some point later the results of the work are retrieved with a corresponding EndXx method.  If the asynchronous work throws an exception, that exception is then propagated out of the call to the EndXx method when the EndXx method is invoked.  This, of course, counts on the EndXx method being invoked.  If the developer makes a mistake such that End is never invoked, any exception that occurred as part of the asynchronous invocation will likely go unnoticed, as would have happened in more situations in .NET 1.x.

With Tasks in .NET 4.0, we face a similar situation as does the APM pattern.  A Task represents an asynchronous operation that may throw an unhandled exception, but unlike work items generated by ThreadPool.QueueUserWorkItem, a Task instance is used to later join with the asynchronous work.  As such, any unhandled exceptions will be stored into the relevant Task instance, later thrown any time that Task is waited on (and also available through the Task’s Exception property).  In fire-and-forget scenarios, where the developer has no intention of joining with the Task, or if the developer simply forgets to join with the Task, the exception may never be observed, and thus if we did nothing special, we’d be in a bad situation very much like that which the APM pattern faces with unhandled exceptions.

To address this, Tasks keep track of whether an unhandled exception has been “observed.”  In this context, “observed” means that code has joined with the Task in some fashion in order to at least be made aware of the exception.  This could be calling Wait/WaitAll on the Task.  It could be checking the Task’s Exception property after the Task has completed.  Or it could be using a Task<TResult>’s Result property.  If a Task sees that its exception has been observed in some manner, life is good.  If, however, all references to a Task are removed (making the Task available for garbage collection), and if its exception hasn’t yet been observed, the Task knows that its exception will never be observed.  In such a case, the Task takes advantage of finalization, and uses a helper object to propagate the unhandled exception on the finalizer thread.  With the behavior described earlier, that exception on the finalizer thread will go unhandled and invoke the default unhandled exception logic, which is to log the issue and crash the process.

In this manner, we can have our cake and eat it, too.  As with the APM, we make any exceptions that occurred asynchronously available later for the app to retrieve.  And as with work items on the ThreadPool, if an exception goes unhandled, it will cause the process to be torn down.

Of course, there may still be situations where you do want a fire-and-forget task, but where you want to automatically “observe” any unhandled exception, either to log it or something similar, or just to prevent the process from crashing in a situation where you know the exception will be benign.  In that case, you can take advantage of continuations to address your needs.

Consider a Task t.  After creating the Task, I can run code like the following:

t.ContinueWith(c => { var ignored = c.Exception; },
    TaskContinuationOptions.OnlyOnFaulted |
    TaskContinuationOptions.ExecuteSynchronously |
    TaskContinuationOptions.DetachedFromParent);

This code creates a continuation off of Task t that will only be scheduled if Task t completes in a Faulted state, meaning that it completed due to an exception going unhandled.  When this continuation runs, it will observe the Task’s exception, preventing it from getting finalized.  You could even wrap this kind of logic up into an extension method, such as:

public static Task IgnoreExceptions(this Task task)
{
    task.ContinueWith(c => { var ignored = c.Exception; },
        TaskContinuationOptions.OnlyOnFaulted |
        TaskContinuationOptions.ExecuteSynchronously |
        TaskContinuationOptions.DetachedFromParent);
    return task;
}

With this in hand, any time I create a fire-and-forget Task where I want an unhandled exception to be ignored, I can simply tag on IgnoreExceptions, e.g. instead of:

var t = Task.Factory.StartNew(…);

I could write:

var t = Task.Factory.StartNew(…).IgnoreExceptions();

Of course, the exception handling logic in Task exists for good reasons, to prevent unhandled exceptions from going unnoticed, and as such it’s not a great idea to liberally sprinkle use of an IgnoreExceptions extension method like this.  But in some situations, it can be quite useful.

There’s a corollary to IgnoreExceptions which may also be useful.  One of the downsides to the logic we use to tear down the process is that it relies on finalization.  Finalization isn’t guaranteed to occur in a timely fashion, so an exception may go unhandled and it may be some time before the app then crashes; some time is better than never, but it’s still not ideal.  If you want a more timely crash, you could use an extension method like the following:

public static Task FailFastOnException(this Task task)
{
    task.ContinueWith(c => Environment.FailFast(“Task faulted”, c.Exception),
        TaskContinuationOptions.OnlyOnFaulted |
        TaskContinuationOptions.ExecuteSynchronously |
        TaskContinuationOptions.DetachedFromParent);
    return task;
}

Rather than ignoring the exception, this uses Environment.FailFast to immediately crash the process.  In this manner, I could write code like the following:

var t = Task.Factory.StartNew(…).FailFastOnException();

and as soon as Task t faults, the process will crash.  This could be beneficial when debugging, as you’ll know immediately when something goes wrong.  There are lots of variations on this as well.  Debugger.Break could be used to immediately break into an attached debugger, you could raise an event of your choosing, the exception could be logged to an application log file, and so forth.  In this manner, continuations are quite powerful.

(For anyone interested in extending Parallel Extensions with extension methods like this, the Parallel Extensions Extras project in the Beta 1 samples available at http://code.msdn.microsoft.com/ParExtSamples includes a plethora of interesting and useful examples. Enjoy!)

Leave a Comment
  • Please add 5 and 5 and type the answer here:
  • Post
  • PingBack from http://microsoft-sharepoint.simplynetdev.com/tasks-and-unhandled-exceptions/

  • PingBack from http://blog.cwa.me.uk/2009/06/01/the-morning-brew-358/

  • I'm confused by this behavior.  Being the author of a C# library that does many of the things that TPL does and having applied it for years to server-side application development, I really don't want an unhandled exception to tear down my server.  It's true the exception would need to be logged, but ongoing operations of the server is more important than failing to handle an exception.  Since the downfall of the server app would be brought upon by the omission of a continuation and the context of the terminal exception is the finalizer, it seems almost impossible to reconcile cause-and-effect for a large system.  Is there a way to suppress this behavior application wide?

  • Thanks for your feedback, Steve.  The same mechanism used to suppress the crash-on-unhandled-exception today can be used to suppress this as well: adding the legacyUnhandledExceptionPolicy flag to the app's configuration file.

    What behavior would you hope to see instead?  i.e. If you were designing the system, what would your ideal be?  Any and all ideas are welcome.  We're already pondering some additions/alternatives.

  • I'd like to see TPL fire a static event on the Task class called UnhandledException when an unhandled exception is detected on a task. If this event is handled, and the handler sets a "Handled" property on its event args to true, it should NOT take down the application.

    legacyUnhandledExceptionPolicy is not the answer: I don't want to IGNORE exceptions - I want the ability to log them and then decide FOR MYSELF, based on the exception, whether the app should continue or shut down! Also, it's not always possible to use a configuration file.

    Forcing down the whole PROCESS via the finalizer thread is totally unworkable in some situations. (The fact that unhandled exceptions on ordinary threads causes unstoppable process shutdown doesn't make it right, although AT LEAST it doesn't usually happen on the finalizer thread). I fail to see how it's a good thing that all objects should have their finalization suppressed because some Task wasn't properly exception-handled.

    Joe

  • Thank you for the feedback, Joe.  We are aware of the concerns here, and we are actively exploring alternatives, including implementations like the one you also suggested.

  • Thanks, Stephen - good to know!

  • Joe, I just reread your comment... what do you mean by "I fail to see how it's a good thing that all objects should have their finalization suppressed because some Task wasn't properly exception-handled"?

  • Hmm. I tried to use IgnoreExceptions(), but it throws TaskCanceledException if the operation succeeds, and can't make it work.

  • Koji, what throws TaskCanceledException?  Can you show the code you're using?

  • Related question: How about calling EndInvoke on a delegate?

    msdn.microsoft.com/.../2e08f6yc.aspx says "always call EndInvoke to complete your asynchronous call.", but your article suggests that the documentation is not always to be trusted.

    I played it safe and recently refactored some old code to use Task instead, but was I wasting my time?

  • Rune, you should be calling EndInvoke; I'm not sure what you mean about my post suggesting otherwise, but if you don't call EndInvoke, at the very least you won't get the results from the operation and you won't get any exceptions that occurred from the operation.  Regardless, using Task instead of asynchronous delegate invocation via BeginInvoke/EndInvoke is the right thing to do.

  • Very useful article, thanks!

    One question:

    There is no TaskContinuationOptions.DetachedFromParent is .NET 4.0 (I guess the article is based on different version.) Is the example working without DetachedFromParent option? Would be good to see an updated version!

  • @Balint: This blog post was written based on a beta of .NET 4.  Prior to its RTM, the default was swapped such that DetachedFromParent is now the default, so you can just remove the DetachedFromParent from these code samples and they'll behave as intended.  If you actually want the task attached, you can use AttachedToParent.

Page 1 of 1 (14 items)