What is the proper way to propagate exceptions in continuation chains?
t.ContinueWith(t2 =>
{
if(t2.Exception != null)
throw t2.Exception;
/* Other async code. */
})
.ContinueWith(/*...*/);
t.ContinueWith(t2 =>
{
if(t2.IsFaulted)
throw t2.Exception;
/* Other async code. */
})
.ContinueWith(/*...*/);
t.ContinueWith(t2 =>
{
if(t2.Exception != null)
return t2;
/* Other async code. */
})
.ContinueWith(/*...*/);
t.ContinueWith(t2 =>
{
if(t2.IsFaulted)
return t2;
/* Other async code. */
})
.ContinueWith(/*...*/);
t.ContinueWith(t2 =>
{
t2.Wait();
/* Other async code. */
})
.ContinueWith(/*...*/);
t.ContinueWith(t2 =>
{
/* Other async code. */
}, TaskContinuationOptions.NotOnFaulted) // Don't think this one works as expected
.ContinueWith(/*...*/);
When an exception is raised, the exception-propagation mechanism takes control. The normal control flow of the program stops, and Python looks for a suitable exception handler. Python's try statement establishes exception handlers via its except clauses.
Exceptions are propagated when you use one of the static or instance Task. Wait methods, and you handle them by enclosing the call in a try / catch statement. If a task is the parent of attached child tasks, or if you are waiting on multiple tasks, multiple exceptions could be thrown.
The method to choose depends on how often you expect the event to occur. Use exception handling if the event doesn't occur very often, that is, if the event is truly exceptional and indicates an error (such as an unexpected end-of-file). When you use exception handling, less code is executed in normal conditions.
To propagate exceptions from procedures, the RAISINGaddition must usually be used for the definition of the interface of a procedure (except for static constructors and event handlers).
The approach we now use in the openstack.net SDK is the extension methods in CoreTaskExtensions.cs
.
The methods come in two forms:
Then
: The continuation returns a Task
, and Unwrap()
is called automatically.Select
: The continuation returns an object, and no call to Unwrap()
occurs. This method is only for lightweight continuations since it always specifies TaskContinuationOptions.ExecuteSynchronously
.These methods have the following benefits:
AggregateException
).supportsErrors=true
for the extension methods).Task
are executed synchronously and Unwrap()
is called for you.The following comparison shows how we applied this change to CloudAutoScaleProvider.cs, which originally used ContinueWith
and Unwrap
extensively:
https://github.com/openstacknetsdk/openstack.net/compare/3ae981e9...299b9f67#diff-3
The TaskContinuationOptions.OnlyOn...
can be problematic because they cause the continuation to be cancelled if their condition is not met. I had some subtle problems with code I wrote before I understood this.
Chained continuations like this are actually quite hard to get right. By far the easiest fix is to use the new .NET 4.5 await
functionality. This allows you almost to ignore the fact you're writing asynchronous code. You can use try/catch blocks just as you might in the synchronous equivalent. For .NET 4, this is available using the async targeting pack.
If you're on .NET 4.0, the most straightforward approach is to access Task.Result
from the antecendent task in each continuation or, if it doesn't return a result, use Task.Wait()
as you do in your sample code. However, you're likely to end up with a nested tree of AggregateException
objects, which you'll need to unravel later on in order to get to the 'real' exception. (Again, .NET 4.5 makes this easier. While Task.Result
throws AggregateException
, Task.GetAwaiter().GetResult()
—which is otherwise equivalent—throws the underlying exception.)
To reiterate that this is actually not a trivial problem, you might be interested in Eric Lippert's articles on exception handling in C# 5 async code here and here.
If you don't want to do anything in particular in the event of an exception (i.e. logging) and just want the exception to be propagated then just don't run the continuation when exceptions are thrown (or in the event of cancellation).
task.ContinueWith(t =>
{
//do stuff
}, TaskContinuationOptions.OnlyOnRanToCompletion);
If you explicitly want to handle the case of an exception (perhaps to do logging, change the exception thrown to be some other type of exception (possibly with additional information, or to obscure information that shouldn't be exposed)) then you can add a continuation with the OnlyOnFaulted
option (possibly in addition to a normal case continuation).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With