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

不羁的心 提交于 2019-11-27 08:05:42

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

I know I'm late but i found this neat little trick which does what you want. Since the full set of exceptions are available with on awaited Task, calling this Task's Wait or a .Result will throw an aggregate exception.

    static void Main(string[] args)
    {
        var task = Run();
        task.Wait();
    }
    public static async Task Run()
    {

        Task[] tasks = new[] { CreateTask("ex1"), CreateTask("ex2") };
        var compositeTask = Task.WhenAll(tasks);
        try
        {
            await compositeTask.ContinueWith((antecedant) => { }, TaskContinuationOptions.ExecuteSynchronously);
            compositeTask.Wait();
        }
        catch (AggregateException aex)
        {
            foreach (var ex in aex.InnerExceptions)
            {
                Console.WriteLine(ex.Message);
            }
        }
    }

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

Exception Handling (Task Parallel Library)

I could say more but it would just be padding. Play with it, it does work as they say. You just have to be careful.

maybe you want this

God (Jon Skeet) explains await exception handling

(personally i shy away from await, but thats just my preference)

in response to comments (too long for a comment reply)

Then use threads as your starting point for an analogous argument as the best practises there will be the source of ones for here.

Exceptions happily get swallowed unless you implement code to pass them out (for instance the async pattern that the await is preumably wrapping ... you add them to an event args object when you raise an event). When you have a scenario where you fire up an arbitrary number of threads and execute on them you have no control over order or the point at which you terminate each thread. Moreover you would never use this pattern if an error on one was relevant to another. Therefor you are strongly implying that execution of the rest is completley independent - IE you are strongly implying that exceptions on these threads have already been handled as exceptions. If you want to do something beyond handling exceptions in these threads in the threads they occur in (which is bizzarre) you should add them to a locking collection that is passed in by reference - you are no longer considering exceptions as exceptions but as a piece of information - use a concurrent bag, wrap the exception in the info you need to identify the context it came from - which would of been passed into it.

Don't conflate your use cases.

I don't know if I am allowed to summarize the two best solutions (in my opinion) of the problem presented in this very interesting question, the answers from Mr Stephen Cleary and Mr adonthy. I changed slightly the API by adding a parameter preserveAggregate, in an attempt to be consistent with the single build-in configuration option, the ConfigureAwait. These configurations are chainable, for example:

await Task.WhenAll(tasks).ConfigureException(true).ConfigureAwait(false);

Here is Mr Stephen Cleary's version (slightly modified):

public static class TaskConfigurationExtensions
{
    public static async Task ConfigureException(this Task task, bool preserveAggregate)
    {
        try
        {
            await task.ConfigureAwait(false); // Because the context doesn't have to be resumed on to throw.
        }
        catch
        {
            if (preserveAggregate) throw task.Exception;
            throw;
        }
    }
    public static async Task<T> ConfigureException<T>(this Task<T> task, bool preserveAggregate)
    {
        try
        {
            return await task.ConfigureAwait(false);
        }
        catch
        {
            if (preserveAggregate) throw task.Exception;
            throw;
        }
    }
}

And here is Mr adonthy's version (also slightly modified):

public static class TaskConfigurationExtensions
{
    private static void Empty<T>(T value) { }
    public static async Task ConfigureException(this Task task, bool preserveAggregate)
    {
        if (preserveAggregate)
        {
            await task
                .ContinueWith(Empty, TaskContinuationOptions.ExecuteSynchronously)
                .ConfigureAwait(false);
            task.Wait();
            return;
        }
        await task.ConfigureAwait(false);
    }
    public static async Task<T> ConfigureException<T>(this Task<T> task, bool preserveAggregate)
    {
        if (preserveAggregate)
        {
            await task
                .ContinueWith(Empty, TaskContinuationOptions.ExecuteSynchronously)
                .ConfigureAwait(false);
            return task.Result;
        }
        return await task.ConfigureAwait(false);
    }
}

As far as I can tell both implementations are equivalent and excellent.

I don't want to give up the practice to only catch the exceptions I expect. This leads me to the following extension method:

public static async Task NoSwallow<TException>(this Task task) where TException : Exception {
    try {
        await task;
    } catch (TException) {
        var unexpectedEx = task.Exception
                               .Flatten()
                               .InnerExceptions
                               .FirstOrDefault(ex => !(ex is TException));
        if (unexpectedEx != null) {
            throw new NotImplementedException(null, unexpectedEx);
        } else {
            throw task.Exception;
        }
    }
}

The consuming code could go like this:

try {
    await Task.WhenAll(tasks).NoSwallow<MyException>();
catch (AggregateException ex) {
    HandleExceptions(ex);
}

A bone-headed exception will have the same effect as in synchronous world, even in case it is thrown concurrently with a MyException by chance. The wrapping with NotImplementedException helps to not loose the original stack trace.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!