How to preserve await behavior with TaskCompletionSource.SetException?

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

Jon Skeet
people
quotationmark

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

people

See more on this question at Stackoverflow