Eduasync part 11: More sophisticated (but lossy) exception handling

(This post covers projects 13-15 in the source code.)

Long-time readers of this blog may not learn much from this post – it’s mostly going over what I’ve covered before. Still, it’s new to Eduasync.

Why isn’t my exception being caught properly?

Exceptions are inherently problematic in C# 5. There are two conflicting aspects:

  • The point of the async feature in C# 5 is that you can write code which mostly looks like its synchronous code. We expect to be able to catch specific exception types as normal.
  • Asynchronous code may potentially have multiple exceptions "at the same time". The language simply isn’t designed to deal with that in the synchronous case.

Now if the language had been designed for asynchrony to start with, perhaps exception flow would have been designed differently – but we are where we are, and we all expect exceptions to work in a certain way.

Let’s make all of this concrete with a sample:

private static void Main(string[] args)
{
    Task<int> task = FetchOrDefaultAsync();
    Console.WriteLine("Result: {0}", task.Result);
}

private static async Task<int> FetchOrDefaultAsync()
{
    // Nothing special about IOException here
    try
    {
        Task<int> fetcher = Task<int>.Factory.StartNew(() => { throw new IOException(); });
        return await fetcher;
    }
    catch (IOException e)
    {
        Console.WriteLine("Caught IOException: {0}", e);
        return 5;
    }
    catch (Exception e)
    {
        Console.WriteLine("Caught arbitrary exception: {0}", e);
        return 10;
    }
}

Here we have a task which will throw an IOException, and some code which awaits that task – and has a catch block for IOException.

So, what would you expect this to print? With the code we’ve got in Eduasync so far, we get this:

Caught arbitrary exception: System.AggregateException: One or more errors occurred. —> System.IO.IOException: I/O error occurred.

Result: 10

If you run the same code against the async CTP, you get this:

Caught IOException: System.IO.IOException: I/O error occurred.

Result: 5

Hmm… we’re not behaving as per the CTP, and we’re not behaving as we’d really expect the normal synchronous code to behave.

The first thing to work out is which boundary we should be fixing. In this case, the problem is between the async method and the task we’re awaiting, so the code we need to fix is TaskAwaiter<T>.

Handling AggregateException in TaskAwaiter<T>

Before we can fix it, we need to work out what’s going on. The stack trace I hid before actually show this reasonably clearly, with this section:

at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceled Exceptions)
at System.Threading.Tasks.Task`1.get_Result()
at Eduasync.TaskAwaiter`1.GetResult() …TaskAwaiter.cs:line 40
at Eduasync.Program.<FetchOrDefaultAsync>d__2.MoveNext() …Program.cs:line 37

So AggregateException is being thrown by Task<T>.Result, which we’re calling from TaskAwaiter<T>.GetResult(). The documentation for Task<T>.Result isn’t actually terribly revealing here, but the fact that Task<T>.Exception is of type AggregateException is fairly revealing.

Basically, the Task Parallel Library is built with the idea of multiple exceptions in mind – whereas our async method isn’t.

Now the team in Microsoft could have decided that really you should catch AggregateException and iterate over all the exceptions contained inside the exception, handling each of them separately. However, in most cases that isn’t really practical – because in most cases there will only be one exception (if any) and all that looping is relatively painful. They decided to simply extract the first exception from the AggregateException within a task, and throw that instead.

We can do that ourselves in TaskAwaiter<T>, like this:

try
{
    return task.Result;
}
catch (AggregateException aggregate)
{
    if (aggregate.InnerExceptions.Count > 0)
    {
        // Loses the proper stack trace. Oops. For workarounds, see
        // See http://bradwilson.typepad.com/blog/2008/04/small-decisions.html
        throw aggregate.InnerExceptions[0];
    }
    else
    {
        // Nothing better to do, really…
        throw;
    }
}

As you can tell from the comment, we end up losing the stack trace using this code. I don’t know exactly how the stack trace is preserved in the real Async CTP, but it is. I suspect this is done in a relatively obscure way at the moment – it’s possible that for .NET 5, there’ll be a cleaner way that all code can take advantage of.

This code is also pretty ugly, catching the exception only to rethrow it. We can check whether or not the task has faulted using Task<T>.Status and extract the AggregateException using Task<T>.Exception instead of forcing it to be thrown and catching it. We’ll see an example of that in a minute.

With our new code in place, we can catch the IOException in our async code very easily.

What if I want all the exceptions?

In certain circumstances it really makes sense to collect multiple exceptions. This is particularly true when you’re waiting for multiple tasks to complete, e.g. with TaskEx.WhenAll in the Async CTP. This caused me a certain amount of concern for a while, but when Mads came to visit in late 2010 and we talked it over, we realized we could use the compositional nature of Task<T> and the convenience of TaskCompletionSource to implement an extension method preserving all the exceptions.

As we’ve seen, when a task is awaited, its AggregateException is unwrapped, and the first exception rethrown. So if we create a new task which adds an extra layer of wrapping and make our code await that instead, only the extra layer will be unwrapped by the task awaiter, leaving the original AggregateException. To come up with a new task which "looks like" an existing task, we can simply create a TaskCompletionSource, add a continuation to the original task, and return the completion source’s task as the wrapper. When the continuation fires we’ll set the appropriate result on the completion source – cancellation, an exception, or the successful result.

You may expect that we’d have to create a new AggregateException ourselves – but TaskCompletionSource.SetException will already do this for us. This makes it looks like the code below isn’t performing any wrapping at all, but remember that Task<T>.Exception is already an AggregateException, and calling TaskCompletionSource.SetException will wrap it in another AggregateException. Here’s the extension method in question:

public static Task<T> WithAllExceptions<T>(this Task<T> task)
{
    TaskCompletionSource<T> tcs = new TaskCompletionSource<T>();

    task.ContinueWith(ignored =>
    {
        switch (task.Status)
        {
            case TaskStatus.Canceled:
                tcs.SetCanceled();
                break;
            case TaskStatus.RanToCompletion:
                tcs.SetResult(task.Result);
                break;
            case TaskStatus.Faulted:
                // SetException will automatically wrap the original AggregateException
                // in another one. The new wrapper will be removed in TaskAwaiter, leaving
                // the original intact.
                tcs.SetException(task.Exception);
                break;
            default:
                tcs.SetException(new InvalidOperationException("Continuation called illegally."));
                break;
        }
    });

    return tcs.Task;
}

Here you can see the cleaner way of reacting to a task’s status – we don’t just try to fetch the result and catch any exceptions; we handle each status individually.

I don’t know offhand what task scheduler is used for this continuation – it may be that we’d really want to specify the current task scheduler for a production-ready version of this code. However, the core idea is sound.

It’s easy to use this extension method within an async method, as shown here:

private static async Task<int> AwaitMultipleFailures()
{
    try
    {
        await CauseMultipleFailures().WithAllExceptions();
    }
    catch (AggregateException e)
    {
        Console.WriteLine("Caught arbitrary exception: {0}", e);
        return e.InnerExceptions.Count;
    }
    // Nothing went wrong, remarkably!
    return 0;
}

private static Task<int> CauseMultipleFailures()
{
    // Simplest way of inducing multiple exceptions
    Exception[] exceptions = { new IOException(), new ArgumentException() };
    TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
    tcs.SetException(exceptions);
    return tcs.Task;
}

Note that this will work perfectly well with the Async CTP and should be fine with the full release as well. I wouldn’t be entirely surprised to find something similar provided by the framework itself by release time, too.

Conclusion

It’s worth being aware of the impedance mismatch between the TPL and async methods in C# 5, as well as how this mismatch is handled. I dislike the idea of data loss, but I can see why it’s being handled in this way. It’s very much in line with the approach of trying to make asynchronous methods look like synchronous ones as far as possible.

We’ll probably look at the compositional nature of tasks again later in the series, but this was one simple example of how transparent it can be – a simple extension method can change the behaviour to avoid the risk of losing exception information when you’re expecting that multiple things can go wrong.

It’s worth remembering that this behaviour is very specific to Task and Task<T>, and the awaiter types associated with them. If you’re awaiting other types of expressions, they may behave differently with respect to exceptions.

Before we leave the topic of exceptions, there’s one other aspect we need to look at – what happens when an exception isn’t observed

11 thoughts on “Eduasync part 11: More sophisticated (but lossy) exception handling”

  1. To preserve the stacktrace you could call the internal instance method Exception.PrepForRemoting(). You would need to use reflection, and you will get a “Server stack trace:” and a “Exception rethrown at [0]:”, but at least the users of your lib won’t be pulling hair when they’re troubleshooting.

    Like

  2. An idea would be to use the ConfigureAwait method to configure exception behavior. That functionality should be added to that method.

    Like

  3. Potentially, could the rewriter do the AggregateException unwrapping and call all the matching catch blocks for you? That would seem *close* the expected behaviour, if overly complex.

    Like

  4. You said, “I don’t know exactly how the stack trace is preserved in the real Async CTP,” but the article linked from your code comment (http://bradwilson.typepad.com/blog/2008/04/small-decisions.html) seems to give an answer: You can preserve exception stack traces by setting the Exception._remoteStackTraceString field through reflection. This is exactly what the PrepForRemoting() method (mentioned by Patrick) does. Could it be that the CTP is simply manipulating _remoteStackTraceString directly?

    Like

  5. @aaron: I don’t know – don’t forget that it’s currently an external assembly just like any other, so it would have to do things via reflection if so. It’s entirely possible that it’s doing that though.

    Like

  6. @Simon: Possibly. I wouldn’t be surprised to find that the team had looked at that as a possibility :) It would tie it to Task though somewhat – or rely on other awaiters throwing AggregateException too.

    Like

  7. @Harry: That would have “worked” as far as I know – but I’m not sure it’s desirable in terms of consistency.

    Like

Leave a comment