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.

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:

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);
}

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
(orTask<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:

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