C# Async Tips and Tricks Part 2 : Async Void
TL;DR: This article discusses the differences between async Task and async void, and how async void methods and async void lambdas, used outside the DispatcherSynchronizationContext, can crash the process if exceptions are not handled.
You can also read the part 3, Tasks and the Synchronization Context.
In the first part of the series, we discussed the behavior of async methods. The second part discusses how async Task and async void methods can differ in behavior, while seemingly being similar.
Authoring Async Methods
Async methods can be authored in three different ways:
async Task MyMethod() { }
which creates a method that can be awaited, but does not return any value,
async Task<T> MyReturningMethod { return default(T); }
which creates a method that can be awaited, and returns a value of the type T,
async void MyFireAndForgetMethod() { }
which is allows for fire and forget methods, and cannot be awaited.
You may be wondering why there are two ways to declare a void returning method. Read on.
[more]
async void methods
async void methods exist for the single purpose of being used as an event handler. More specifically, it has been introduced to support UI elements event handlers, nothing else.
It allows writing simple async methods such as this one:
private async void Button1_Click(object sender, EventArgs args) { }
All Microsoft provided events respect a BCL guideline for which all .NET event delegates return void, which is in part why the async Task declaration cannot not be used.
An async void method is handled differently under the hood by the compiler. Task returning async methods are managed by the AsyncTaskMethodBuilder class, whereas async void methods are managed by the AsyncVoidMethodBuilder class.
Both builders handle the synchronization context capture the same way, where the ambient context is reused to execute each block of code between the awaits of an async method.
This means than in normal circumstances, the code executes the same way in both Task and void returning methods.
But there is one fundamental difference between async Task and async void: Exceptions handling.
async Task methods and Exceptions
Let’s consider this example, using an async Task:
static void Main(string[] args) { Test(); Console.ReadLine(); } public static async Task Test() { throw new Exception(); }
When executed, this program will not end until a key is pressed. The exception will be stored in an anonymous Task instance, because the return value of call to Test is never awaited, nor stored in a variable.
It is a pretty bad practice to write this kind of code, because the exception is essentially silent, and may hide a bug. The compiler is a bit helpful in that regard to avoid writing that kind of code, and will provide you with the following warning message:
warning CS4014: Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.
As a best practice, you should never have this warning anywhere in your code, and handle exceptions properly.
TPL unhandled exceptions
The TPL, which used behind async methods, handles exceptions differently than the ThreadPool, and unobserved exceptions will be thrown in the UnobservedTaskException event.
If we modify a bit our sample:
static void Main(string[] args) { TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; Test(); Console.ReadLine(); GC.Collect(); GC.WaitForPendingFinalizers(); Console.ReadLine(); } static void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) { Console.WriteLine(e.Exception); } public static async Task Test() { throw new Exception(); }
We’ll notice that the event is not raised immediately after the task is in a faulted state. The task instance must be collected before, which is why forcing a garbage collection will raise the event.
As a best practice, you should always add a handler to the UnobservedTaskException as a safety net to log those exceptions, and probably treat them as potential bugs.
async void methods and Exceptions
Now we’ll be using the same code, but change the async Task to an async void method:
static void Main(string[] args) { Test(); Console.ReadLine(); } public static async void Test() { throw new Exception(); }
First, you’ll notice that the compiler warning that was present with async Task is no longer shown.
Second, when run, the whole process crashes.
The reason behind this is the Synchronization Context used by the AsyncVoidMethodBuilder, being none in this example. When there is no ambient Synchronization Context, any exception that is unhandled by the body of an async void method is rethrown on the ThreadPool.
While there is seemingly no other logical place where that kind of unhandled exception could be thrown, the unfortunate effect is that the process is being terminated, because unhandled exceptions on the ThreadPool effectively terminate the process since .NET 2.0. You may intercept all unhandled exception using the AppDomain.UnhandledException event, but there is no way to recover the process from this event.
To make the matter a bit more difficult, this event is not available in Metro apps, because the AppDomain class has been removed.
Async lambdas
Unless you consider yourself as reliable enough to never forget to place a try catch block in all async void methods, you should never use an async void method anywhere but in UI event handlers, such as behind button clicks and alike.
You could make a code review and search for all async void methods using a find and replace... but you won’t find this one:
new List<int>().ForEach(async i => { throw new Exception(); });
The method signature for List<T>.ForEach is :
public void ForEach(Action<T> action);
for which it is valid to provide and async void lambda, putting the process at risk of termination.
This kind of code is very easy to write when moving stateful code to async-only APIs, and you may crash your process without being warned that you’re writing dangerous code.
An other interesting fact about these async void lambdas is that the compiler, when facing these two overloads :
public void ForEach(Action<T> action); public void ForEach(Func<T, Task> action);
with this call :
ForEach(async i => { });
Will automatically choose the Task returning Func<>.
async void is for UI Event Handlers Only
When writing UI event handlers, async void methods are somehow painless because exceptions are treated the same way found in non-async methods; they are thrown on the Dispatcher. There is a possibility to recover from such exceptions, with is more than correct for most cases.
Outside of UI event handlers however, async void methods are somehow dangerous to use and may not that easy to find.
This is why I’ve been using a custom Static Analysis (fxcop) rule that prevents the use of the AsyncVoidMethodBuilder class in my apps, effectively banning both explicit and implicit async void methods.
Most of this code is using an MVVM approach where ICommand data-binding is used, therefore limiting the use of any async void methods.
What to Take Away
Here is what to remember about async Task/void methods:
- async void methods are only suited for UI event handlers
- async void methods outside the dispatcher raise unhandled exceptions on the thread pool, which terminate the process
- async void lambdas are difficult to find, but easy to write. You should be looking for any use of the AsyncVoidMethodBuilder in your generated IL
- You should always be using or awaiting the return value of an async method (treat CS4014 as an error)
- Always add a handler to UnobservedTaskException event, to log exceptions in case the previous point is not respected.
I’ll probably soon be publishing a simple code source for the custom static analysis rule that prevents the use AsyncVoidMethodBuilder class, to streamline the process of banning async void methods.