1

When calling into async code like for example productUpdate.UpdateAsync(...), there are chances that it could throw an AggregateException having multiple inner exceptions or just one exception. This all depends on how UpdateAsync was implemented internally.

Question: Since await unwraps only the first exception in the list of exceptions within an AggregateException, the following special code tries to circumvent that, but this is clunky and ideally in every place where I am calling into some external library's async code, there could be an AggregateException with multiple exceptions. Would it make sense to have this in all those places? (sure probably could move into a helper method but that's not the point here) and also then there's the question of what meaningful things I am doing by catching these exceptions. I think it does NOT make sense in all places. Your thoughts?

var t1 = FooAsync();
var t2 = BarAsync();

var allTasks = Task.WhenAll(t1, t2);

try
{
    await allTasks;
}
catch (Exception)
{
    var aggEx = allTasks.Exception as AggregateException;
    // todo: do something with aggEx.InnerExceptions
}

Update: Added whole repro code here for user Dave and the result of running it:

using System;
using System.Threading.Tasks;

class Example
{
    static void Main()
    {
        BlahAsync().Wait();
    }

    static async Task BlahAsync()
    {
        var t1 = FooAsync(throwEx: true);
        var t2 = BarAsync(throwEx: true);

        var allTasks = Task.WhenAll(t1, t2);

        try
        {
            await allTasks;
        }
        catch (AggregateException agex)
        {
            Console.WriteLine("Caught an aggregate exception. Inner exception count: " + agex.InnerExceptions.Count);
        }
    }

    static async Task FooAsync(bool throwEx)
    {
        Console.WriteLine("FooAsync: some pre-await code here");

        if (throwEx)
        {
            throw new InvalidOperationException("Error from FooAsync");
        }

        await Task.Delay(1000);

        Console.WriteLine("FooAsync: some post-await code here");
    }

    static async Task BarAsync(bool throwEx)
    {
        Console.WriteLine("BarAsync: some pre-await code here");

        if (throwEx)
        {
            throw new InvalidOperationException("Error from BarAsync");
        }

        await Task.Delay(1000);

        Console.WriteLine("BarAsync: some post-await code here");
    }
}

Result:

FooAsync: some pre-await code here
BarAsync: some pre-await code here

Unhandled Exception: System.AggregateException: One or more errors occurred. (Error from FooAsync) ---> System.InvalidOperationException: Error from FooAsync
   at Example.<FooAsync>d__2.MoveNext() in C:\Users\foo\source\repos\ConsoleApp9\ConsoleApp9\UnderstandingCallStack.cs:line 37
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Example.<BlahAsync>d__1.MoveNext() in C:\Users\foo\source\repos\ConsoleApp9\ConsoleApp9\UnderstandingCallStack.cs:line 20
   --- End of inner exception stack trace ---
   at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
   at System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken)
   at System.Threading.Tasks.Task.Wait()
   at Example.Main() in C:\Users\foo\source\repos\ConsoleApp9\ConsoleApp9\UnderstandingCallStack.cs:line 8
aspnetuser
  • 121
  • 7
  • 1
    First thing you can do is explicitly catch the aggregate exception `catch(AggregateException ae) { //handle it }` – Dave May 21 '18 at 21:11
  • Nope, that doesn't work as `await` unwraps the exception. – aspnetuser May 21 '18 at 21:36
  • If it never throws an aggregate exception because you await it, then your cast in your catch block will always fail. It's either an aggregate exception or it isn't – Dave May 21 '18 at 21:40
  • well the cast won't 'fail' it will just always be null – Dave May 21 '18 at 21:42
  • Also await takes the first inner exception of the aggregate exception that it receives, however there is nothing to say that the first exception isn't also an Aggregate Exception. In this scenario you're awaited Task will throw an Aggregate Exception – Dave May 21 '18 at 21:47
  • 1
    @Dave, I added a repro code catching the AggregateException and you can see you still cannot catch an AggregateException here. If I catch an InvalidOperationException here (which is the first exception or second exception) , thisn work as expected. – aspnetuser May 21 '18 at 21:51
  • Please don't think I'm being condescending, but you've got you're wires a bit crossed. If you call Wait() on a Task it will throw an AggregateException (AE from now on). In your async method BlahAsync, you await all Tasks, This will unwrap the aggregate exception and throw a InvalidOp Exception from one of your tasks (probs the first). However this exception is not handled and so carries on up the call stack, where we meet the BlahAsync.Wait() in the Main function. Calling Wait() on a Task will always throw an AE – Dave May 21 '18 at 21:57
  • 1
    Ah you are right. Sorry for the confusion. – aspnetuser May 21 '18 at 22:01
  • 1
    No worries, tasks and async can get a bit confusing. If you are awaiting your task why are trying to catch an AE anyway? They are only really thown when you wait on asynchronous code, in a synchronous manner, using .Wait() or .Result on the returned Task. Are you trying to effectively get the all the exceptions from the aggregate exception rather then first one that await unwraps for you? – Dave May 21 '18 at 22:04
  • I realise now that I misread you're question and havent really answered what you were after, potentially. Check out this question which I think is what you were getting at https://stackoverflow.com/questions/18314961/i-want-await-to-throw-aggregateexception-not-just-the-first-exception – Dave May 21 '18 at 22:06
  • 1
    Yeah, I am imagining a scenario where I wrote a library that external users can use where I have an async abstraction(ex: ` IProductValidator.ValidateAsync`) that users would implement. In this case I was wondering how should I deal with exceptions, so asked the question here :) – aspnetuser May 21 '18 at 22:08

1 Answers1

1

As mentioned in my comment you can explicitly catch an Aggregate exception like so

try
{
    //do you're async code
}
catch (AggregateException ae)
{
    //handle it
}

you can add additional catch statements to deal with other exception types, but remember always start with the most specific exception type first and if you have a catch all exception handler (which can be argued you shoudn't but thats not for now) that should always be the last catch statement.

Now when you can catch an AggregateException, as you say, it can contain many exceptions and each of those can contain inner exceptions as well, so it has the potential to be a complete structure of nested exceptions. But don't fear! .NET has provided help. You can called .Flatten() on your aggregate exception. This will return you a new Aggreagete exception that has a flat list of its inner exceptions, not a nested list, so you can easily, iterate over it or just take the top one, whatever you need to do.

try
{
    //async code stuff
}
catch(AggregateException ae)
{
    var allExceptions = ae.Flatten().InnerExceptions;
    // now do what you want with the list of exceptions
}
catch (Exception x)
{
    // ooo look here is an example of the second catch statement catching any other exception that isnt an AggregateException)
}

Now another cool thing you can do with aggregate exceptions when you catch them, is pass a custom exception handler to the Handle method on the AE instance. This is useful if you want handle specific kinds of exceptions such as a FileNotFoundException but if there are any other exceptions that should be thrown as another Aggregate Exception for the caller to handle. Like this

try
{
    //async stuff
}
catch (AggregateException ae)
{
    ae.Handle(x => 
        {
            if (x is FileNotFoundException)
            {
                Console.WriteLine("I handled the file not found, don't worry");
                return true;
            }
            return false
        });
}

What happens here is that each exception in the Aggregate Exceptions inner exceptions is passed to the handler, we return true, if we handle it and false if we don't. All the exceptions that were not handled (ie we returned false for) are added as the inner exceptions of a new Aggregate exception.

This last part might not be relevant to you, if you just want to the handle the Aggregate exception whatever it contains, but its a good thing to have in the tool box IMO

Dave
  • 2,829
  • 3
  • 17
  • 44