Using async void
is generally discouraged, but in some situations itās unavoidable. In this post we take a look at the internals of async void
and find ways to use this notorious .NET construct safely.
Table of contents
Open Table of contents
Why does nobody like async void?
Consider the following piece of fairly simply code:
try
{
await AsyncTask();
}
catch (Exception e)
{
Console.WriteLine($"Caught exception: {e.Message}");
}
// Block so we can wait for async operations to complete
Console.WriteLine("Press Enter to exit");
Console.ReadLine();
static async Task AsyncTask()
{
Console.WriteLine("About to throw...");
await Task.Delay(200);
throw new Exception("The expected exception");
}
You can probably guess whatās going to happen. Hereās the output:
About to throw...
Caught exception: The expected exception
Press Enter to exit
So far so goodā¦ because, so far, weāve been playing with the nice well brought up kids from the š async Task
family.
Letās introduce a troublemaker to the mix, from the š async void
family:
try
{
AsyncTask();
}
catch (Exception e)
{
Console.WriteLine($"Caught exception: {e.Message}");
}
// Block so we can wait for async operations to complete
Console.WriteLine("Press Enter to exit");
Console.ReadLine();
static async void AsyncTask()
{
Console.WriteLine("About to throw...");
await Task.Delay(200);
throw new Exception("The expected exception");
}
This code is almost identicalā¦ only two things have changed:
- We changed from
async Task
toasync void
- Instead of calling
await AsyncTask()
we just callAsyncTask()
, sinceasync void
is not awaitable
Hereās the output from the new code:
About to throw...
Press Enter to exit
Unhandled exception. System.Exception: The expected exception
And this is why async void
has such a bad rep. Our try..catch
block didnāt work at all. The exception from the async void
method skipped right past our try..catch
block and crashed our applicationā¦ preventing any possible graceful recovery from the situation. How rude! š
If you can then, just donāt use async void
. Use async Task
and all will be well.
Why donāt we just get rid of this atrocity?
If async void
is so evil, why not just get rid of it then? It turns out that there are some specific situations where async void is necessary:
-
Event Handlers: Event handlers in UI applications are expected to return void, so making them asynchronous requires the use of async void to allow awaiting operations within the handler without blocking the UI thread.
-
Overriding void Methods: If you have to override a method that returns void and you need to perform asynchronous operations within that override, you might be forced to use async void.
-
Async Flows in Constructors: There may be times when you want to initiate an asynchronous flow (without needing to wait for its completion) during object construction.
-
Fire-and-Forget: If you really want to just fire-and-forget.
How can we stop async void
from being so nasty?
When you canāt avoid async void
, there are ways to beat it into submission and stop it from being so nasty.
Wrap it all in try..catch
The simplest and most common technique is to wrap all the code inside your async void
method in a try..catch
block to make sure that any unhandled exceptions donāt crash your application:
static async void AsyncTask()
{
try
{
Console.WriteLine("About to throw...");
await Task.Delay(200);
throw new Exception("The expected exception");
}
catch (Exception e)
{
Debug.WriteLine("Unhandled exception in AsyncTask: {0}", e.Message);
}
}
The above works and itās really simpleā¦ and I could have left my investigations at that (which is what most sensible people would do). You could stop reading now then, and this will be a really boring blog post.
But what if someone else wrote the async void method you need to call? And canāt we just get these things to behave a bit more like our well behaved async Task
methods? These questions kept me up for at least 30 seconds one night and it seemed like the kind of completely unnecessary challenge that would serve as a thinly veiled excuse to dig into the internals of async
in .NET, so in this blog post weāre going to explore an alternative solution. š
Looking under the hood
Letās dig into async void
to see if we can understand it a little betterā¦
async void
First, letās take a look at a simplified version of our code in SharpLab. SharpLab allows us to see how async void
gets compiled into executable code.
What we find is that the compiler turns our async void
method into a state machine that uses AsyncVoidMethodBuilder. All of the code we wrote in our async void
method sits in the MoveNext
method in that state machine. You can see itās all wrapped in a try..catch
block and if an exception is caught in that block then AsyncVoidMethodBuilder.SetException
is called:
catch (Exception exception)
{
<>1__state = -2;
<>t__builder.SetException(exception);
}
So this is how exceptions get handled for async void
methods.
AsyncVoidMethodBuilder
The source code for AsyncVoidMethodBuilder
is pretty interesting.
The current SynchronizationContext
gets saved in the constructor. Later, in the SetException
method, the exception is āthrown asynchronouslyā on that SynchronizationContext.
ThrowAsync
ThrowAsync
calls SynchronizationContext.Post, passing the exception information in the state:
// Post the throwing of the exception to that context, and return.
targetContext.Post(state => ((ExceptionDispatchInfo)state).Throw(), edi);
Now we have some options!
Global Exception Handler
Armed with the above, building a custom SynchronizationContext
to help us catch any errors from our async void
code is relatively straight forward:
using System.Runtime.ExceptionServices;
namespace AsyncVoid;
public class ExceptionHandlingSynchronizationContext(Action<Exception> exceptionHandler, SynchronizationContext? innerContext)
: SynchronizationContext
{
public override void Post(SendOrPostCallback d, object? state)
{
if (state is ExceptionDispatchInfo exceptionInfo)
{
exceptionHandler(exceptionInfo.SourceException);
return;
}
if (innerContext != null)
{
innerContext.Post(d, state);
return;
}
base.Post(d, state);
}
}
Our ExceptionHandlingSynchronizationContext
class inherits from SynchronizationContext
(which means we can assign it as an active context). It also takes an Action<Exception>
in the constructor and overrides the Post
method to invoke that exception handler whenever an exception is postedā¦ easy as!
Using the context is also very simple. Hereās an updated version of our code that uses this new custom context:
using AsyncVoid;
SynchronizationContext.SetSynchronizationContext(new ExceptionHandlingSynchronizationContext(Handler, SynchronizationContext.Current));
AsyncTask();
// Block so we can wait for async operations to complete
Console.WriteLine("Press Enter to exit");
Console.ReadLine();
Console.WriteLine("Bye!");
static async void AsyncTask()
{
Console.WriteLine("About to throw...");
await Task.Delay(200);
throw new Exception("The expected exception");
}
void Handler(Exception exception)
{
Console.WriteLine("Caught exception: " + exception.Message);
}
And this time, when we run the application, the exception from our async void
is handled gracefully by our Handler
and our application continues to run happily. Hereās the output:
About to throw...
Press Enter to exit
Caught exception: The expected exception
Bye!
Very coolā¦ although this approach does have itās limitations. For one thing, thereās only one exception handlerā¦ what if we wanted to call multiple async void
methods and have different exception handling logic for each of them?
Custom exception handlers
Applying different exception handlers to different async void
code blocks should be pretty straight forward. We know that the AsyncVoidMethodBuilder
will post any exceptions to the SynchronizationContext
that was current when the async void
code was invokedā¦ so we just need to wrap different async void
code blocks in different instances of ExceptionHandlingSynchronizationContext
.
We can define a static utility method to help with this:
static void RunAsyncVoidSafely(Action task, Action<Exception> handler)
{
var syncCtx = SynchronizationContext.Current;
try
{
SynchronizationContext.SetSynchronizationContext(new ExceptionHandlingSynchronizationContext(handler, syncCtx));
task();
}
finally
{
SynchronizationContext.SetSynchronizationContext(syncCtx);
}
}
And now we can execute as many async void
methods as we like, each with their own custom exception handling logic.
Hereās an example:
using AsyncVoid;
RunAsyncVoidSafely(AsyncTask1, exception => Console.WriteLine("Caught one!"));
RunAsyncVoidSafely(AsyncTask2, exception => Console.WriteLine("Caught another!"));
// Block so we can wait for async operations to complete
Console.WriteLine("Press Enter to exit");
Console.ReadLine();
Console.WriteLine("Bye!");
static async void AsyncTask1()
{
await Task.Delay(200);
throw new Exception("Une exception");
}
static async void AsyncTask2()
{
await Task.Delay(200);
throw new Exception("Super exceptional");
}
And the output of that program (which may vary, depending on which AsyncTask gets scheduled on the ThreadPool first):
Press Enter to exit
Caught another!
Caught one!
Bye!
Very cool!
Conclusion
The golden rule remains, āPrefer async Task over async voidā. But by spending some time with this notorious .NET villain weāve come to understand it a little better and actually itās not as big and mean as people say. There are ways to leverage async void
safely (albeit somewhat advanced techniques).
The next time you really want to run some code that is āfire and forgetā then, youāll be able to do so with style and without worrying about it shutting down your application!