Search code examples
c#task-parallel-libraryc#-5.0async-await

I want await to throw AggregateException, not just the first Exception


When awaiting a faulted task (one that has an exception set), await will rethrow the stored exception. If the stored exception is an AggregateException it will rethrow the first and discard the rest.

How can we use await and at the same time throw the original AggregateException so that we do not accidentally lose error information?

Note, that it is of course possible to think of hacky solutions for this (e.g. try-catch around the await, then call Task.Wait). I really wish to find a clean solution. What is the best-practice here?

I thought of using a custom awaiter but the built-in TaskAwaiter contains lots of magic that I'm not sure how to fully reproduce. It calls internal APIs on TPL types. I also do not want to reproduce all of that.

Here is a short repro if you want to play with it:

static void Main()
{
    Run().Wait();
}

static async Task Run()
{
    Task[] tasks = new[] { CreateTask("ex1"), CreateTask("ex2") };
    await Task.WhenAll(tasks);
}

static Task CreateTask(string message)
{
    return Task.Factory.StartNew(() => { throw new Exception(message); });
}

Only one of the two exceptions is thrown in Run.

Note, that other questions on Stack Overflow do not address this specific problem. Please be careful when suggesting duplicates.


Solution

  • I disagree with the implication in your question title that await's behavior is undesired. It makes sense in the vast majority of scenarios. In a WhenAll situation, how often do you really need to know all of the error details, as opposed to just one?

    The main difficulty with AggregateException is the exception handling, i.e., you lose the ability to catch a particular type.

    That said, you can get the behavior you want with an extension method:

    public static async Task WithAggregateException(this Task source)
    {
      try
      {
        await source.ConfigureAwait(false);
      }
      catch
      {
        // source.Exception may be null if the task was canceled.
        if (source.Exception == null)
          throw;
    
        // EDI preserves the original exception's stack trace, if any.
        ExceptionDispatchInfo.Capture(source.Exception).Throw();
      }
    }