Control flow redux: exceptions in asynchronous code

Warning: as ever, this is only the result of reading the spec and experimentation. I may well have misinterpreted everything. Eric Lippert has said that he'll blog about exceptions soon, but I wanted to put down my thoughts first, partly to see the difference between what I've worked out and what the real story is.

So far, I've only covered "success" cases - where tasks complete without being cancelled or throwing exceptions. I'm leaving cancellation for another time, but let's look at what happens when exceptions are thrown by async methods.

What happens when an async method throws an exception?

There are three types of async methods:

  • Ones that are declared to return void
  • Ones that are declared to return Task
  • Ones that are declared to return Task<T> for some T

The distinction between Task and Task<T> isn't important in terms of exceptions. I'll call async methods that return Task or Task<T> taskful methods, and ones that return void taskless methods. These aren't official terms and they're not even nice terms, but I don't have any better ones for the moment.

It's actually pretty easy to state what happens when an exception is thrown - but the ramifications are slightly more complicated:

  • If code in a taskless method throws an exception, the exception propagates up the stack
  • If code in a taskful method throws an exception, the exception is stored in the task, which transitions to the faulted state
    • If we're still in the original context, the task is then returned to the caller
    • If we're in a continuation, the method just returns

The inner bullet points are important here. At any time it's executing, an async method is either still in its original context - i.e. the caller is one level up the stack - or it's in a continuation, which takes the form of an Action delegate. In the latter case, we must have previously returned control to the caller, usually returning a task (in the "taskful method" case).

This means that if you call a taskful method, you should expect to be given a task without an exception being thrown. An exception may well be thrown if you wait for the result of that task (possibly via an await operation) but the method itself will complete normally. (Of course, there's always the possibility that we'll run out of memory while constructing the task, or other horrible situations. I think it's fair to classify those as pathological and ignore them for most applications.)

A taskless method is much more dangerous: not only might it throw an exception to the original caller, but it might alternatively throw an exception to whatever calls the continuation. Note that it's the awaiter that gets to determine that for any await operation... it may be an awaiter which uses the current SynchronizationContext for example, or it may be one which always calls the continuation on a new thread... or anything else you care to think of. In some cases, that may be enough to bring down the process. Maybe that's what you want... or maybe not. It's worth being aware of.

Here's a trivial app to demonstrate the more common taskful behaviour - although it's unusual in that we have an async method with no await statements:

using System;
using System.Threading.Tasks;

public class Test
{
    static void Main()
    {
        Task task = GoBangAsync();
        Console.WriteLine("Method completed normally");
        task.Wait();
    }
    
    static async Task GoBangAsync()
    {
        throw new Exception("Bang!");
    }
}

And here's the result:

Method completed normally

Unhandled Exception: System.AggregateException: One or more errors occurred. 
        ---> System.Exception: Bang!
   at Test.<GoBangAsync>d__0.MoveNext()
   --- 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 Test.Main()

As you can see, the exception was only thrown when we waited for the asynchronous task to complete - and it was wrapped in an AggregateException which is the normal behaviour for tasks.

If an awaited task throws an exception, that is propagated to the async method which was awaiting it. You might expect this to result in an AggregateException wrapping the original AggregateException and so on, but it seems that something is smart enough to perform some unwrapping. I'm not sure what yet, but I'll investigate further when I get more time. EDIT: I'm pretty sure it's the EndAwait code used when you await a Task or Task<T>. There's certainly no mention of AggregateException in the spec, so I don't believe the compiler-generated code does any of this.

How eagerly can we validate arguments?

If you remember, iterator blocks have a bit of a usability problem when it comes to argument validation: because the iterator code is only run when the caller first starts iterating, it's hard to get eager validation. You basically need to have one non-iterator-block method which validates the arguments, then calls the "real" implementation with known-to-be valid arguments. (If this doesn't ring any bells, you might want to read this blog post, where I'm coming up with an implementation of LINQ's Where method.)

We're in a similar situation here, if we want arguments to be validated eagerly, causing an exception to be thrown directly to the caller. As an example of this, what would you expect this code to do? (Note that it doesn't involve us writing any async methods at all.)

using System;
using System.Net;
using System.Threading.Tasks;

class Test
{
    static void Main()
    {
        Uri uri = null;
        Task<string> task = new WebClient().DownloadStringTaskAsync(uri);
    }
}

It could throw an exception eagerly, or it could set the exception into the return task. In many cases this will have a very similar effect - if you call DownloadStringTaskAsync as part of an await statement, for example. But it's something you should be aware of anyway, as sometimes you may well want to call such methods outside the context of an async method.

In this particular case, the exception is thrown eagerly - so even though we're not trying to wait on a task, the above program blows up. So, how could we achieve the same thing?

First let's look at the code which wouldn't work:

// This will only throw the exception when the caller waits
// on the returned task.
public async Task<string> DownloadStringTaskAsync(Uri uri)
{
    if (uri == null)
    {
        throw new ArgumentNullException("uri");
    }
        
    // Good, we've got an argument... now we can use it.
    // Real implementation goes here.
    return "Just a dummy implementation";
}

The problem is that we're in an async method, so the compiler is writing code to catch any exceptions we throw, and propagate them through the task instead. We can get round this by using exactly the same trick as with iterator blocks - using a first non-async method which then calls an async method after validating the arguments:

public Task<string> DownloadStringTaskAsync(Uri uri)
{
    if (uri == null)
    {
        throw new ArgumentNullException("uri");
    }
        
    // Good, we've got an argument... now we can use it.
    return DownloadStringTaskAsyncImpl(uri);
}
    
public async Task<string> DownloadStringTaskAsyncImpl(Uri uri)
{
    // Real implementation goes here.
    return "Just a dummy implementation";
}

There's a nicer solution though - because C# 5 allows us to make anonymous functions (anonymous methods or lambda expressions) asynchronous too. So we can create a delegate which will return a task, and then call it:

public Task<string> DownloadStringTaskAsync(Uri uri)
{
    if (uri == null)
    {
        throw new ArgumentNullException("uri");
    }
        
    // Good, we've got an argument... now we can use it.
    Func<Task<string>> taskBuilder = async delegate {
        // Real implementation goes here.
        return "Just a dummy implementation";
    };
    return taskBuilder();
}

This is slightly neater for methods which don't need an awful lot of code. For more involved methods, it's quite possibly worth using the "split the method in two" approach instead.

Conclusion

The general exception flow in asynchronous methods is actually reasonably straightforward - which is a good job, as normally error handling in asynchronous flows is a pain.

You need to be aware of the consequences of writing (or calling) a taskless asynchronous method... I expect that the vast majority of asynchronous methods and delegates will be taskful ones.

Finally, you also need to work out when you want exceptions to be thrown. If you want to perform argument validation, decide whether it should throw exceptions eagerly - and if so, use one of the patterns shown above. (I haven't spent much time thinking about which approach is "better" yet - I generally like eager argument validation, but I also like the consistency of all errors being propagated through the task.)

Next up: dreaming of multiple possibly faulting tasks.

Published Mon, Nov 1 2010 18:50 by skeet
Filed under: , ,

Comments

# re: Control flow redux: exceptions in asynchronous code

Ian Griffiths also has a nice blog post on the same theme, see www.interact-sw.co.uk/.../csharp5-async-exceptions.

Monday, November 01, 2010 12:56 PM by Matt Warren

# re: Control flow redux: exceptions in asynchronous code

I was sure the fact that the exception was thrown before the first await would be enough for it to propagate to the caller. As you know, I even mentioned that that's a good change from the way iterators worked. I would be *very* disappointed if in the final version the compiler would catch exceptions before the first await.

I'm not talking about cases such as

async void Foo() {

 if (something which is false) {

    await something;

 }

 exception here;

 await something else;

}

I'm talking about the simpler case where the exception is thrown before any await appeared in the code.

Monday, November 01, 2010 3:15 PM by configurator

# re: Control flow redux: exceptions in asynchronous code

The case for eager parameter validation for iterators is fairly clear: delaying validation makes debugging harder since iterators are typically consumed in a context where exception catching is tricky.  For instance, if any iterator in a linq query throws, the whole thing comes down, and it's very hard to try...catch it.

The case for Tasks is much less clear.  In fact, do we want eager exceptions at all?  There's a downside too: you can't eagerly validate everything, and doing validation eagerly makes the programming model more complex: suddenly there's an extra exit mode:  the Task might (a) not terminate, (b) complete successfully (c) fail, (d) fail before starting and throw an exception.

What does option (d) provide that (c) couldn't just as well?  Notice that (d) has some pretty nasty side effects; generally, when you start one or more tasks, you assume execution will continue *until* you do something with those tasks.  Any eagerly thrown exception violates that assumption and alters control flow in a hard-to-manage fashion.

Let's say, for instance, you're trying to preload uri as the user types it: whenever a keypress registers two tasks are started - one to retrieve the url, one to look in a local cache; whenever a task completes, a status message is updated.

Using eager validation, you must wrap each individual task creation in an exception and deal with errors there; that makes task creation longer and it means some duplication of status message update code.  You *also* need to validate task return values since a valid url may not be downloadable for a host of reasons.

Using validation as part of the task, you keep your error handling code in one spot.

Finally, eager validation doesn't compose nicely at all.  If I write a method "FromCacheOrDownloadAsync" and follow the API convention of eager validation, do I need to eagerly validate parameters to sub-tasks too?  If so, I can't do that with C#'s async keyword and must again resort to manually dealing with tasks (which is kind of the point to *avoid*, right?)  And in any case, *which* parameter errors are being eagerly validated anyhow?

I think eager parameter validation for tasks (such as DownloadAsync) is a bad idea, in general.

Tuesday, November 02, 2010 4:10 AM by Eamon Nerbonne