12

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.


  • Did you see this blog entry explaining why they chose to implement exception handling for await like they did? blogs.msdn.com/b/pfxteam/archive/2011/09/28/10217876.aspx - Matthew Watson
  • Yes, thanks. I kind of agree with that reasoning for the general case, but my use case is not covered. - usr
  • "In all cases, the Task’s Exception property still returns an AggregateException that contains all of the exceptions, so you can catch whichever is thrown and go back to consult Task.Exception when needed." - Timbo

4 답변


20

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, it's easy to get the behavior you want with an extension method:

public static async Task WithAggregateException(this Task source)
{
    try
    {
        await source.ConfigureAwait(false);
    }
    catch
    {
        throw source.Exception;
    }
}


  • Getting all exceptions is important for diagnostics. I don't ever want to miss nullrefs and so on. They must be logged. Handling an exception is a different scenario though. Btw, the solution works fine. Thanks. - usr
  • If you're talking about fatal or boneheaded exceptions, I recommend using the top-level handlers (UnobservedTaskException, Application handlers) instead of repeated try/catch blocks. - Stephen Cleary
  • I agree mostly. The scenario I have in mind is a top-level error handler indeed (all it does is log crashes). But the full crash info must somehow arrive there. UnobservedTaskException and Application handlers do not guarantee that in the presence of await because errors might end up observed but swallowed.; I get very nervous when I see errors swallowed. I have seen too many cases where production suffered from severe errors for month and nobody noticed. - usr
  • UnobservedTaskException is still raised for unobserved exceptions (it just doesn't crash the process anymore). The only case where you have an observed, swallowed exception is WhenAll, and it only happens when you have multiple exceptions, one of which is not swallowed. You really do have to try to ignore exceptions with await. - Stephen Cleary
  • @BatteryBackupUnit: Task has two primary uses: one as a "future" for asynchronous code, and the other as a "unit of work" for parallel code. I think your approach would require a mutable/freezable AggregateException at least. Feel free to post your idea for discussion. Aside: technically, await doesn't unwrap the AggregateException, but that's an OK mental model. - Stephen Cleary

3

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


3

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.


  • The first link does not deal with await. The 2nd one contains a valid answer. Thanks. I will let other answers come in for a few days because I want to discover the best solution. - usr
  • the thing is i dont agree with your use case. I think when you are using await you are assuming that each task handles its own exceptions - await tells you that something has broken in an operation where you cannot cancel the other tasks (ie they must be independant). Or at least i think that is assumed in its pattern. Ifyou want to handle exceptions outside of the task then you should use the top pattern. I probably should of explained that. Also if you want aggregate exception - the question - the first should be your choice. - John Nicholas
  • The top pattern involves blocking which is an entirely different use case than await addresses. A valid use case but not mine - I require async IO and non-blocking here.; Also, tasks do not necessarily handle their own errors. If that was the case we'd never need error propagation. Errors are supposed to propagate so that the normal flow of control is aborted. All I want is all errors, not just an arbitrary one. - usr
  • updated ... ofc a task handles its own errors ... a task it a unit of encapsulation around a unit of work. The work of handling its exceptions I would argue is most definatley a part of that or else you are creating a locking nightmare for yourself as 100 threads all throw exceptions and block on one piece of code - unless you make it have no state at which point u would realise that it essentially sits in the task itself anyway ... or compromise with me and pass in a lambda to handle the exception. - John Nicholas
  • @JohnNicholas reference to god's article is broken ( - Konstantin Chernov

0

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 NotIplementedException helps to not loose the original stack trace.

Linked


Related

Latest