18

(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:

  1. await will throw the first inner exception.
  2. All exceptions are still available through 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.

usr
  • 168,620
  • 35
  • 240
  • 369
  • 1
    It is always fun to read a new question from a 50k+ rep user, 90% of the time I go "ooh, I'd like to know that too" and end up favoriting the question – Scott Chamberlain Feb 15 '16 at 16:00
  • 1
    If it's the `await` semantics you want with regards to exceptions, why not just go straight to the source (http://referencesource.microsoft.com/#mscorlib/system/runtime/compilerservices/TaskAwaiter.cs,ca9850c71672bd54) and copy what `TaskAwaiter` does? – Kirill Shlenskiy Feb 15 '16 at 16:08
  • @KirillShlenskiy this uses internal APIs. Also, the caller of my code shouldn't need to know anything special. – usr Feb 15 '16 at 16:15
  • @usr: `I'm trying to construct a function that will copy the result of one task over to a TaskCompletionSource.` Feel free to copy [either](https://github.com/StephenCleary/AsyncEx.Tasks/blob/master/src/Nito.AsyncEx.Tasks/TaskCompletionSourceExtensions.cs#L12) of [mine](https://github.com/StephenCleary/AsyncEx/blob/master/Source/Nito.AsyncEx%20(NET45%2C%20Win8%2C%20WP8%2C%20WPA81)/TaskCompletionSourceExtensions.cs#L14). – Stephen Cleary Feb 15 '16 at 16:35
  • 1
    @StephenCleary mine is now modeled in this spirit. Meanwhile, I also reviewed Jon's public code. Apparently, everybody except me knew to use the right overload. Getting every nuance right with TPL code can be a black art. – usr Feb 15 '16 at 16:55
  • @usr, would something like this be also relevant to you scenario? `var proxyTcs = new TaskCompletionSource>(); proxyTcs.SetResult(faultedTask); await proxyTcs.Task.Unwrap();` – noseratio Feb 15 '16 at 21:17
  • 1
    @Noseratio that works too, as I just tested. Preferring the cleaner SetException approach, though. – usr Feb 15 '16 at 21:22
  • Indeed. I wasn't aware of that overload too, and I forgot the fact that `Task.Exception` is in fact `AggregateException`, so `Exception.InnerExceptions != null` always. What about stack traces, are they also nicely preserved this way? – noseratio Feb 15 '16 at 21:36
  • 2
    @Noseratio stack trace is OK with both methods, Unwrap has one useless section in it. – usr Feb 15 '16 at 22:28
  • For anyone else who arrived here due to .Result appearing to throw exceptions other than AggregateException -- note that ValueTask.Result behaves differently from Task.Result. Task.Result always wraps the exception as an AggregateException, while ValueTask.Result does not – NotEnoughData Aug 23 '22 at 04:49

1 Answers1

10

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);
Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194