(This is a new attempt at this question which now demonstrates the issue better.)
Let's say we have a faulted task (var faultedTask = Task.Run(() => { throw new Exception("test"); });
) and we await it. await
will unpack the AggregateException
and throw the underlying exception. It will throw faultedTask.Exception.InnerExceptions.First()
.
According to the source code for ThrowForNonSuccess
it will do this by executing any stored ExceptionDispatchInfo
presumably to preserve nice stack traces. It will not unpack the AggregateException
if there is no ExceptionDispatchInfo
.
This fact alone was surprising to me because the documentation states that the first exception is always thrown: https://msdn.microsoft.com/en-us/library/hh156528.aspx?f=255&MSPPError=-2147217396 It turns out that await
can throw AggregateException
, though, which is not documented behavior.
This becomes a problem when we want to create a proxy task and set it's exception:
var proxyTcs = new TaskCompletionSource<object>();
proxyTcs.SetException(faultedTask.Exception);
await proxyTcs.Task;
This throws AggregateException
whereas await faultedTask;
would have thrown the test exception.
How can I create a proxy task that I can complete at will and that will mirror the exception behavior that the original task had?
The original behavior is:
await
will throw the first inner exception.Task.Exception.InnerExceptions
. (An earlier version of this question left out this requirement.)Here's a test that summarizes the findings:
[TestMethod]
public void ExceptionAwait()
{
ExceptionAwaitAsync().Wait();
}
static async Task ExceptionAwaitAsync()
{
//Task has multiple exceptions.
var faultedTask = Task.WhenAll(Task.Run(() => { throw new Exception("test"); }), Task.Run(() => { throw new Exception("test"); }));
try
{
await faultedTask;
Assert.Fail();
}
catch (Exception ex)
{
Assert.IsTrue(ex.Message == "test"); //Works.
}
Assert.IsTrue(faultedTask.Exception.InnerExceptions.Count == 2); //Works.
//Both attempts will fail. Uncomment attempt 1 to try the second one.
await Attempt1(faultedTask);
await Attempt2(faultedTask);
}
static async Task Attempt1(Task faultedTask)
{
var proxyTcs = new TaskCompletionSource<object>();
proxyTcs.SetException(faultedTask.Exception);
try
{
await proxyTcs.Task;
Assert.Fail();
}
catch (Exception ex)
{
Assert.IsTrue(ex.Message == "test"); //Fails.
}
}
static async Task Attempt2(Task faultedTask)
{
var proxyTcs = new TaskCompletionSource<object>();
proxyTcs.SetException(faultedTask.Exception.InnerExceptions.First());
try
{
await proxyTcs.Task;
Assert.Fail();
}
catch (Exception ex)
{
Assert.IsTrue(ex.Message == "test"); //Works.
}
Assert.IsTrue(proxyTcs.Task.Exception.InnerExceptions.Count == 2); //Fails. Should preserve both exceptions.
}
The motivation for this question is that I'm trying to construct a function that will copy the result of one task over to a TaskCompletionSource
. This is a helper function that's used often when writing task combinator functions. It's important that API clients cannot detect the difference between the original task and a proxy task.
TaskCompletionSource<T> is used in a variety of cases when an operation's lifetime is controlled manually, for instance, in different communication protocols. So, let's mimic one of them. Let suppose we want to create a custom database adapter.
Is it safe to pass non-thread-safe objects created on one thread to another using TaskCompletionSource. SetResult()? Yes, as long as the object can be used on a different thread than the one it was created on (of course).
When we "await" a Task, it unwraps the AggregateException to give us the inner exception. This makes our code easier to program, and often there will only be one inner exception. But when there are multiple exceptions, we lose visibility to them. This topic came up in a virtual training class that I did this past week.
It turns out that await can throw AggregateException, though, which is not documented behavior.
No, that's the behaviour when the first nested exception is an AggregateException
.
Basically, when you call TaskCompletionSource.SetException(Exception)
, it wraps that exception in an AggregateException
.
If you want to preserve multiple exceptions, just use the overload of SetException
which accepts an IEnumerable<Exception>
:
proxyTcs.SetException(faultedTask.Exception.InnerExceptions);
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