Skip to content

Catching async void exceptions in C#

Published: at 07:29 PM

Catching Async Void

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:

  1. We changed from async Task to async void
  2. Instead of calling await AsyncTask() we just call AsyncTask(), since async 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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!