Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

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.

like image 628
usr Avatar asked Aug 19 '13 13:08

usr


People also ask

Does await throw AggregateException?

When using await, it's going to unwrap the first exception and return it, that's why we don't hit the catch (AggregateException e) line. But if we use something like the below code sample, we catch the AggregateException , note that it's not a good idea since we're blocking the running thread.

Does task WhenAll throw exception?

WhenAll only throws the first exception of the AggregateException stored in task. Exception , even when multiple tasks have faulted.

What happens if an exception is thrown within an asynchronous method?

As we know, in asynchronous programming, control does not wait for the function's result and it executes the next line. So when the function throws an exception, at that moment the program control is out of the try-catch block.

How do you handle exceptions thrown by tasks?

Exceptions are propagated when you use one of the static or instance Task. Wait methods, and you handle them by enclosing the call in a try / catch statement. If a task is the parent of attached child tasks, or if you are waiting on multiple tasks, multiple exceptions could be thrown.


4 Answers

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();
  }
}
like image 68
Stephen Cleary Avatar answered Sep 27 '22 18:09

Stephen Cleary


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); });
    }
like image 25
adonthy Avatar answered Sep 27 '22 18:09

adonthy


Here is a shorter implementation of Stephen Cleary's WithAggregateException extension method:

public static async Task WithAggregateException(this Task source)
{
    try { await source.ConfigureAwait(false); }
    catch (OperationCanceledException) when (source.IsCanceled) { throw; }
    catch { source.Wait(); }
}

public static async Task<T> WithAggregateException<T>(this Task<T> source)
{
    try { return await source.ConfigureAwait(false); }
    catch (OperationCanceledException) when (source.IsCanceled) { throw; }
    catch { return source.Result; }
}

This approach is based on a suggestion by Stephen Toub in this API proposal in GitHub.


Update: I added a special handling of the cancellation case, to prevent the awkwardness of propagating an AggregateException that contains an OperationCanceledException. Now the OperationCanceledException is propagated directly, and the Task.IsCanceled status is preserved. Kudos to @noseratio for pointing out this flaw in the comments of this answer. Of course now this implementation is not much shorter than Stephen Cleary's approach!

like image 28
Theodor Zoulias Avatar answered Sep 27 '22 19:09

Theodor Zoulias


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.

like image 20
John Nicholas Avatar answered Sep 27 '22 19:09

John Nicholas