###Intro
After puzzling over my code for a while, I discovered that exceptions don't necessarily propagate through ContinueWith
:
int zeroOrOne = 1;
Task.Factory.StartNew(() => 3 / zeroOrOne)
.ContinueWith(t => t.Result * 2)
.ContinueWith(t => Console.WriteLine(t.Result))
.ContinueWith(_ => SetBusy(false))
.LogExceptions();
In this example, the SetBusy
line 'resets' the chain of exceptions, so the divide by zero exception isn't seen and subsequently blows up in my face with "A Task's exception(s) were not observed..."
So... I wrote myself a little extension method (with tons of different overloads, but basically all doing this):
public static Task ContinueWithEx(this Task task, Action<Task> continuation)
{
return task.ContinueWith(t =>
{
if(t.IsFaulted) throw t.Exception;
continuation(t);
});
}
Searching around a bit more, I came across this blog post, where he proposes a similar solution, but using a TaskCompletionSource, which (paraphrased) looks like this:
public static Task ContinueWithEx(this Task task, Action<Task> continuation)
{
var tcs = new TaskCompletionSource<object>();
task.ContinueWith(t =>
{
if(t.IsFaulted) tcs.TrySetException(t.Exception);
continuation(t);
tcs.TrySetResult(default(object));
});
return tcs.Task;
}
###Question
Are these two versions strictly equivalent? Or is there a subtle difference between throw t.Exception
and tcs.TrySetException(t.Exception)
?
Also, does the fact that there's apparently only one other person on the whole internet who's done this indicate that I'm missing the idiomatic way of doing this?
The difference between the two is subtle. In the first example, you are throwing the exception returned from the task. This will trigger the normal exception throwing and catching in the CLR, the ContinueWith
will catch and wrap it and pass it to the next task in the chain.
In the second you are calling TrySetException
which will still wrap the exception and pass it to the next task in the chain, but does not trigger any try/catch logic.
The end result after one ContinueWithEx
is AggregateException(AggregateException(DivideByZeroException))
. The only difference I see is that the inner AggregateException has a stack trace set in the first example (because it was thrown) and no stack trace in the second example.
Neither is likely to be significantly faster than the other, but I would personally prefer the second to avoid unneeded throws.
I have done something like this where the continuation returned a result. I called it Select
, handled cases of the previous task being cancelled, provided overloads to modify the exception instead of or in addition to the result, and used the ExecuteSynchronously
option. When the continuation would itself return a Task, I called that Then
instead based on the code from this article
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