The Problem with C# 5's async/await Pattern

By on 5/21/2012

C# 5 brings a fantastic new feature … built-in asynchrony (not to be confused with concurrency). The compiler has added two new keywords, async and await, which allows your code to transparently change execution contexts. For example, instead of writing:
  .StartNew(() => MakeSomeDecisionSlowly())
  .ContinueWith(result => ProcessResult(result));
You can simply say:
bool result = await MakeSomeDecisionSlowly();
While the two pieces of code are exactly equivalent, the second code is so much simpler to understand. Because you don't really have to think about the fact that there is an asynchronous context switch going on, you are free to focus on your program's logic, rather than worrying about manually orchestrating what tasks are doing what.

However, after using these features for several days while porting my KhanAcademy windows phone app to windows 8, I've come to approach them with a bit of caution. While they are great for low level IO-bound tasks, in my opinion they don't scale well to higher level API design. A few things to watch out for:

If you use await somewhere in your code, then the containing method needs to be marked with the async keyword, and there are specific rules about return types. Namely, the method must either be void, or return Task<T>. For example, the example above would have to be in a method
public async void DoThatThing() { … }
Or, if you wanted to return the result of 'MakeSomeDecisionSlowly' back to the caller, it would have to look like this:
public async Task<bool> DoThatThing() { … }
Which is great, and all, but then it means that the calling method must be decorated with 'async' as well. If you are refactoring an existing codebase, this can have the effect of rippling up the inheritance chain with numerous (albeit small) changes … depending on the situation.

Ok, fine, maybe that's not a huge deal; it's kind of annoying but I can definitely deal with that kind of refactoring. The big problem I have with async/await is that outside of relatively trivial pieces of code, it can tend to wipe away many benefits of proper abstractions.

Let's say you are implementing a remote API call, and you have several requirements:
  • Process the results if
    • You have a network connection (common mobile requirement)
    • The server is reachable (what if you're connected at starbucks, but haven't accepted the wifi's terms of use?)
    • The server returns a code 200 (bugs happen)
    • The server indicates that your credentials are ok (what if the user's password expired, or was changed by the user on your website?)
    • The server's json response indicates a successful request
  • If any of the above conditions are false, notify the user of the problem (with specific text for the situation).
This is a very common scenario in any occasionally connected mobile application, and you can see that there are no less than five distinct decisions that need to be made during the course of an API call. If you use async/await blindly, you have two problems:

First, you are deferring these decisions to the caller … you'll do a nice asynchronous call, and return a Task<string> with the json results; or if you're sophisticated, a Task<T> with the results already parsed and converted to a strongly typed object. However, it's up to the caller to handle exceptions, check for network connections, deal with individual response scenarios (logged out, invalid request, etc.). So you may end up with every call site looking like this:
if (HasConnection())
    MyObject result = await MakeRemoteAPICall();
  catch(Exception e)
  ProcessError("No Connection");
Second, providing further abstractions is difficult because you are limited to a single return type. So if you want to handle all of the stated requirements for the caller, you will have to wrap up the result in a special return value that can provide some details to the user, such as 'was it successful', the error message if it wasn't, and the parsed return value if it was. While the pros and cons of using error codes for return values have been debated for ages; I tend to prefer simpler return values for local APIs, since a complex return value adds an additional burden on the caller to know how to interpret the results. At best, every call site would look something like this:
Result<MyObject> value = await MakeRemoteAPICall();
if (value.WasSuccessful)
That's a bit better than before, but still rather verbose for my taste.

I propose that the callback style of API design still has a place for situations like this. Consider the following sample:
  result => ProcessSucessfulResult(result),
  error => ProcessError(error));
For the local consumer of this API, this is logically the same: if the call was successful do this, otherwise do that. But the caller doesn't have to worry about all the nuances of deciding whether a call was successful or not. They can focus on how to process the results, rather than dealing with all of this ceremony. And after all, isn't that what we're all aiming to do, simplify our code?

I would love to hear thoughts about how this kind of code can be made even simpler. Would love to hear any opinions, for or against. Thanks!

edit: lively debate over on Reddit :)

See more in the archives