You probably have heard that something is wrong with async void methods. But let’s explore what exactly.

So here is a simple async void method:

public static async void FooBar(CancellationToken token)
{
    await Task.Delay(TimeSpan.FromSeconds(5), token);
}

What’s wrong with it? Well… It’s kind of dangerous. “Your application might crash” kind of dangerous!

Let’s see what will happen if a given token is canceled before the delay is done:

static async void FooBar(CancellationToken token)  
{  
    await Task.Delay(TimeSpan.FromSeconds(5), token);  
}  
  
static void Main(string[] args)  
{  
    try  
    {  
        var cts = new CancellationTokenSource();  
        cts.CancelAfter(TimeSpan.FromSeconds(2));  
        FooBar(cts.Token);  
        Console.ReadLine();  
    }
    catch (Exception e)  
    {
        Console.WriteLine("Error: " + e.Message);  
    }
}

The application will just crash!

Unhandled exception. System.Threading.Tasks.TaskCanceledException: A task was canceled.
   at Leatcode.Runner.Program.FooBar(CancellationToken token) in /Users/sergey/Documents/GitHub/Blogging/Program.cs:line 12
   at System.Threading.Tasks.Task.<>c.<ThrowAsync>b__128_1(Object state)
   at System.Threading.ThreadPoolWorkQueue.Dispatch()
   at System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart()
   at System.Threading.Thread.StartCallback()

Let’s see the lowered C# for the async method to understand why this is happening:

[AsyncStateMachine(typeof(<FooBar>d__0))]
[DebuggerStepThrough]
private static void FooBar(CancellationToken token)
{
    FooBar_d__0 stateMachine = new FooBar_d__0();
    stateMachine.t__builder = AsyncVoidMethodBuilder.Create();
    stateMachine.token = token;
    stateMachine.__state = -1;
    stateMachine.t__builder.Start(ref stateMachine);
}

You probably know, that the C# compiler ( * ) rewrites the async methods into a state machine where each state is dedicated for a block between “awaits”. But the key difference between async Task and async void methods is the underlying type that is generated by the compiler. In the case of async void the compiler uses AsyncVoidMethodBuilder that is stored inside the state machine.


( * ) There is an ongoing initiative called “async2” to re-implement async machinery in the runtime. But regardless of the implementation the async void behavior will be the same.

The most important method in the generated state machine is MoveNext that essentially “resumes” the async method execution when the awaited task is finished:

[CompilerGenerated]
private sealed class FooBar_d__0 : IAsyncStateMachine
{
    public int __state;
    public AsyncVoidMethodBuilder t__builder;
    private void MoveNext()
    {
        int num = __state;
        try
        {
            TaskAwaiter awaiter;
            if (num != 0)
            {
                // When the state is -1 we're running the code
                // from the top of the method to the first await.
                awaiter = Task.Delay(TimeSpan.FromSeconds(5L), token).GetAwaiter();
                if (!awaiter.IsCompleted)
                {
                    // If awaited task is not done, we set the state to '0'
                    // to know were to resume the execution and
                    // register ourselves to be called when the task is done.
                    num = (__state = 0);
                    t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
                    return;
                }
            }
            else
            {
                // Essentially empty since there is no code after the await.
            }
            
            // If the task failed, `GetResult` will throw an exception!
            awaiter.GetResult();
        }
        catch (Exception exception)
        {
            // This is the main thing for us: an exception handling!
            __state = -2;
            t__builder.SetException(exception);
            return;
        }
    }

I simplified the code a bit, but the main things for us are: the call to awaiter.GetResult() which is done inside the try/catch block and the exception handling logic in the catch block that calls __builder.SetException(exception).

Let’s look at AsyncVoidMethodBuilder.SetException method:

public void SetException(Exception exception)
{
    if (m_synchronizationContext != null)
    {
        // If we captured a synchronization context, Post the throwing of the exception to it 
        // and decrement its outstanding operation count.
        try
        {
            AsyncServices.ThrowAsync(exception, targetContext: m_synchronizationContext);
        }
        finally
        {
            NotifySynchronizationContextOfCompletion();
        }
    }
    else
    {
        // Otherwise, queue the exception to be thrown on the ThreadPool.  This will
        // result in a crash unless legacy exception behavior is enabled by a config
        // file or a CLR host.
        AsyncServices.ThrowAsync(exception, targetContext: null);
    }
}

The code is quite readable and it says the following: either post the exception into the captured SynchronizationContext (and in this case rely on the unhandled exception handling mechanism for Windows Forms, WPF etc) or kill the application (unless the legacy exception behavior is enabled by the config).

When async Task fails, then the resulting task stores the exception that can be observed by the caller. And if the caller won’t observe it, TaskScheduler.UnobservedException handler is triggered. But in the case of async void method there is no place to store the error, so the only way to “handle” it is to crash the application!

There are a few legitimate use cases for async void methods, mainly in UI framework for handling the events. And you can claim that it’s quite easy to avoid them since it’s easy to spot them in the code review. But there are some cases when they can be hidden in the code:

static async Task FooBar(CancellationToken token)  
{  
    await Task.Delay(TimeSpan.FromSeconds(5), token);  
}  
  
static void Main(string[] args)  
{  
    var cts = new CancellationTokenSource();  
    cts.CancelAfter(TimeSpan.FromSeconds(2));

    // async void is convertible to 'System.Action'!  
    Action action = async () => await FooBar(cts.Token);  
    action();
    
    var timer = new Timer(async state =>  
    {  
        await FooBar(cts.Token);  
    }, state: null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));  
  
    Parallel.Invoke(  
        async () => await FooBar(cts.Token),  
        async () => await FooBar(cts.Token));
}

There are 3 cases in this code where async void delegates are used and an unhandled exception in any of them will cause an application to crash! The async void is just a regular void method with some compile-time magic involved. It means that you can always store or convert async void delegates to System.Action and this is what is going on in the code above.

There are legitimate use cases for explicit async void methods and async void delegates, but they’re quite rare. And since it’s easy to miss them, I prefer to rely on some sort of static analysis tools that can help detecting them. R# or Rider can detect them, or you can use ErrorProne.NET analyzers (or something similar) to even break the build if someone will accidentally introduce them (and of course, you can suppress the warning if you have a legit use case for using them):

async_void

Just remember, async void methods are not just “fire and forget” methods, but rather “fire and die in flames in case of an error” methods!