Demystifying async void in C#: Why It's Dangerous and When It's Okay

Async operations are an integral part of any application. In C#, mostly asynchronous methods return Task or Task<T>, but there's also the odd case of async void. We know void is for synchronous operations, so why does C# even allow async void? I will dissect this question in today's post by looking at the need and dangers of async void.

Demystifying async void in C#: Why It's Dangerous and When It's Okay

What is async void?

In C#, async void is a method signature that returns nothing on completion or exception. The async keyword allows you to mark a method as an asynchronous, non-blocking operation. However, unlike its popular counterpart async Task or async Task<T>, you cannot await an async void method, and the caller can't observe or manage execution. You also don't get anything in return, so no result or exception can be obtained for further use.

Why async void is dangerous

The dominant problem with async void is the inability to get exceptions. Let's understand it with the help of an example.

Exception handling in an async Task method:

static async Task DangerousTaskMethod()
{
    await Task.Delay(500);
    throw new InvalidOperationException("Boom from async Task!");
}

Exception handling in an async void method:

static async void DangerousVoidMethod()
{
    await Task.Delay(500);
    throw new InvalidOperationException("Boom from async void!");
}

The caller code


// async Task: exceptions are captured in the Task
static async Task DangerousTaskMethod()
{
    await Task.Delay(500);
    throw new InvalidOperationException("Boom from async Task!");
}

Console.WriteLine("\n=== async void example ===");
try
{
    DangerousVoidMethod(); // fire-and-forget
    await Task.Delay(2000); // give time for exception to surface
    Console.WriteLine("After void method");
}
catch (Exception ex)
{
    Console.WriteLine("Caught exception from async void: " + ex.Message);
}

Console.WriteLine("\n=== async Task example ===");
try
{
    await DangerousTaskMethod(); // properly awaited
    Console.WriteLine("After Task method");
}
catch (Exception ex)
{
    Console.WriteLine("Caught exception from async Task: " + ex.Message);
}

Output:

Output

First, we are calling an async void method, then an async Task. Although we kept each of them in try/catch, the DangerousVoidMethod method does not handle the exception, and the application crashes.

Now changing the sequence:

Console.WriteLine("\n=== async Task example ===");
try
{
    await DangerousTaskMethod(); // properly awaited
    Console.WriteLine("After Task method");
}
catch (Exception ex)
{
    Console.WriteLine("Caught exception from async Task: " + ex.Message);
}

Console.WriteLine("\n=== async void example ===");
try
{
    DangerousVoidMethod(); // fire-and-forget
    await Task.Delay(2000); // give time for exception to surface
    Console.WriteLine("After void method");
}
catch (Exception ex)
{
    Console.WriteLine("Caught exception from async void: " + ex.Message);
}
Output

The async Task returns an exception, and the caller can handle it gracefully.

How async Methods work internally

When you mark a method async Task, the compiler does the following:

  • Generates a state machine under the hood.
  • Splits the method at each await.
  • Wraps the continuation in a Task (or Task<T>) so the runtime can track completion and exceptions.

In code, we can roughly represent it as:

public Task DangerousTaskMethod()
{
    var builder = AsyncTaskMethodBuilder.Create();
    var stateMachine = new DoWorkStateMachine();
    stateMachine.Builder = builder;
    stateMachine.Start();
    return builder.Task; // <--- you get a Task back
}

However, when you create an async void method, no Task is returned. The caller cannot await, check for completion or catch exceptions. Rather, the exceptions are "posted" to the current SynchronizationContext (e.g., UI thread dispatcher) instead of flowing back to the caller.

No Way to Await Completion

You cannot await an async void method. If you try:

await DangerousVoidMethod();

It shows the following error:

Error

This is because:

  • No way to track completion like it happens in Task.
  • You can't chain async work after it.
  • If it fails, you cannot retry easily.

Harder to Unit Test

Unit tests rely on awaiting sync methods. Since async void methods cannot be awaited, you are unable to detect completion and propagate exceptions.

Loss of information

The async void keeps the work of the async keyword, meaning the operations will be asynchronous. However, an async void method does not return a value. This leaves you unable to access the result of the method, whether it returns a result or an exception. The loss of information halts coordination among asynchronous operations where other operations need the result of async void methods.

When async void is okay

We know its dangers now, but the main question arises: if it creates so many problems, why does this feature even exist? So, like any tool or feature, async void has scenarios where it fits.

1. Event handlers

Events in .NET expect a void return type. If you need to perform async work in an event handler, you are left with only async void:

private async void Button_Click(object Sender, EventArgs e)
{
    await Task.Delay(1000);
    MessageBox.Show("Done!");
}

2. Top-level fire-and-forget

In rare cases, you might deliberately use async void. Because of its fire-and-forget behaviour, async void is workable in applications such as logging or telemetry, where failure isn't critical and you don't care about completion.

public async void LogInFileAsync(string message)
{
    await File.AppendAllTextAsync("web-log.txt", message);
}

⚠️ Note: async void in this context is risky. A safer approach is to wrap the work in Task.Run and discard the result:

public void LogInFile(string message)
{
    _ = Task.Run(async () =>
    {
        try
        {
            await File.AppendAllTextAsync("web-log.txt", message);
        }
        catch (Exception ex)
        {
            // optionally handle or log exception
        }
    });
}

Best practices for working with async void methods in C#

Prefer async Task mostly

As async void doesn't return the exception, it can lead to application crashes. So, prefer async Task generally, especially in cases where exception handling and error recovery are critical. async Task methods leverage you with a built-in exception handling mechanism.

Handle exception manually with async void

When you are left to rely on async void only, then write try/catch manually in async void methods. This method is a bit verbose, but you can avoid the application behaving unexpectedly due to unhandled errors.

Log and monitor async void

When working with async void methods, keep an eye on them with proper logging to catch any exceptions on time.

Conclusion

async void is an option in C# asynchronous programming. However, it has a specific set of scenarios where it aids developers. If this feature is not used properly, it can cause application crashes and leave unexpected behaviour. We uncovered dangers and their core reason in the post with suitable examples. Also, we discussed scenarios where async void really helps. Consider the best practices to utilize this tool properly.

elmah.io: Error logging and Uptime Monitoring for your web apps

This blog post is brought to you by elmah.io. elmah.io is error logging, uptime monitoring, deployment tracking, and service heartbeats for your .NET and JavaScript applications. Stop relying on your users to notify you when something is wrong or dig through hundreds of megabytes of log files spread across servers. With elmah.io, we store all of your log messages, notify you through popular channels like email, Slack, and Microsoft Teams, and help you fix errors fast.

See how we can help you monitor your website for crashes Monitor your website