What is the proper way to propagate exceptions in continuation chains?
Asked Answered
W

4

14

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(/*...*/);
Watermelon answered 18/3, 2013 at 14:10 Comment(2)
I'm assuming that instead of t.ContinueWith(t =>...) you really mean t.ContinueWith(t2 =>...), right?Bekelja
@Geoff: Yes, that is correct.Watermelon
H
4

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.

Heartache answered 13/3, 2014 at 16:11 Comment(1)
Thank you so much for Task.GetAwaiter().GetResult()!Discerning
P
1

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).

Procephalic answered 18/3, 2013 at 17:47 Comment(2)
The problem I've noticed with this approach is that if I return the resulting task and then continue to add things to the chain, no exception will be propagate from this point forward...Watermelon
i.e. chaining any additional ContinueWith will not propagate exceptions... which is a no good.Watermelon
B
1

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:

  1. Avoids calling the continuation method when the antecedent is faulted or cancelled.
  2. Instead, the result of the antecedent becomes the result of the chained operation (accurately preserving exception information, without wrapping the exception in multiple layers of AggregateException).
  3. Allows callers to write continuation methods that support faulted antecedents, in which case only cancelled antecedent tasks bypass the continuation (specify supportsErrors=true for the extension methods).
  4. Continuations that return a 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

Bonacci answered 13/3, 2014 at 16:23 Comment(0)
S
0

The OnlyOnFaulted option is ment for cases where the previous Task could not execute it's task correctly. One way would be rethrow the exception in every continuation task, but that would be bad design.

If you want to pass the exception just like an ordinary object then treat it as one. Have the Task return the exception and use the Task.Result property in the continuation.

Smithers answered 10/3, 2014 at 22:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.