Why You Should Avoid async void in C#

Avoid Async Void Methods in C#

Ever wonder why your async void method behaves like a rebellious teenager—ignoring exceptions, skipping tests, and never telling you when it’s done? You’re not alone. Many developers stumble into using async void thinking it’s a harmless shortcut. But beneath its innocent look hides a world of pain.

Let’s break down why async void is a pitfall you should avoid and when (if ever) it’s okay to use it.

Understanding async void

In C#, asynchronous methods typically return Task or Task<T>, which allows callers to await their completion. However, an async method can also return void, which is syntactically allowed but semantically dangerous.

public async void DoSomethingAsync()
{
    await Task.Delay(1000);
    Console.WriteLine("Done!");
}

At first glance, this looks like any other async method. But this method can’t be awaited. That means:

  • No way to wait for it to finish.
  • No way to catch exceptions.
  • No way to test it easily.

Let’s dig deeper.

Why async void is Problematic

Exception Handling

When an async method returns a Task, exceptions can be caught using try/catch or by observing the task. But with async void, unhandled exceptions crash your application.

public async void CrashAsync()
{
    throw new InvalidOperationException("Boom");
}

try
{
    CrashAsync(); // No await here
}
catch (Exception ex)
{
    Console.WriteLine($"Caught: {ex.Message}");
}

Result: The catch block never runs. The exception escapes.

Explanation: The CrashAsync() method throws on a thread pool thread, and since it’s void, the exception cannot be caught by the calling method.

Testing Asynchronous Code

Unit tests rely on await to ensure that test logic completes and exceptions are asserted.

[Test]
public async Task TestAsyncMethod()
{
    await DoSomethingAsync(); // if DoSomethingAsync returns Task
}

If the method returns void, the test runner can’t know when it’s done. It might pass before your code even finishes running.

Controlling Operation Completion

When chaining operations or coordinating tasks, you need control. async void offers none.

public async void FireAndForget()
{
    await Task.Delay(1000);
    Console.WriteLine("Done");
}

public async Task MainFlow()
{
    FireAndForget();
    Console.WriteLine("Main flow continues...");
}

Explanation: MainFlow does not wait for FireAndForget, leading to unpredictable timing.

Behavior in User Interfaces

In UI frameworks like WPF or WinForms, async void is often used in event handlers.

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

Here, async void is unavoidable, because event handlers must match a specific void return signature. But even then, you’re walking a tightrope.

  • You can’t catch exceptions thrown in the handler.
  • Long-running operations block the UI thread if not awaited properly.

Proper Use of async void

There is only one valid use case for async void: event handlers.

That’s it.

In every other situation, prefer returning Task:

public async Task DoSomethingAsync()
{
    await Task.Delay(1000);
    Console.WriteLine("Done!");
}

Now it can be:

  • Awaited
  • Caught in try/catch
  • Tested

Guidelines for Proper Use of async void in Event Handlers

If you’re using async void in UI event handlers:

  • Use try/catch inside the method to handle exceptions:
private async void Button_Click(object sender, EventArgs e)
{
    try
    {
        await Task.Delay(1000);
        MessageBox.Show("Operation completed");
    }
    catch (Exception ex)
    {
        MessageBox.Show($"Error: {ex.Message}");
    }
}
  • Avoid long-running logic. Offload work to a service method returning Task.
  • Use loading indicators to maintain UI responsiveness.

FAQ: Common async void Questions

Can I force async void methods to be testable?

Not really. You can refactor the logic into a Task-returning method and call that from the async void.

Why does C# allow async void at all?

Because event handlers require void return types. It’s a necessary evil.

What about fire-and-forget scenarios?

Even then, prefer capturing the Task and logging exceptions. Or use background services for better control.

Conclusion: async void is a Trap (Most of the Time)

The next time you’re tempted to use async void, stop and think: can I return Task instead? If yes, do it. You’ll gain better exception handling, testability, and control.

Use async void only in UI event handlers—and even then, with caution.

Got a war story about async void? Share it in the comments, and let’s learn from each other’s scars.

Leave a Reply

Your email address will not be published. Required fields are marked *