Back to Blog
C#

c# async void: Exception Risks and Safe Patterns

Learn how c# async void behaves, why it can crash your app, and when it's acceptable to use in event handlers.

async-voidC#exception-handlingevent-handlersasync-task
Illustration of an async void method with an unhandled exception causing an application crash, showing a broken chain link.

In C#, async void is a method signature that allows an asynchronous method to return void instead of Task. It exists primarily to support event handlers, but its exception behavior makes it dangerous outside that context. This article explains how c# async void works, why it can crash your application, and when it is the right choice.

What c# async void Does Differently

An async void method is declared like this:

public async void LoadData() { var data = await FetchDataAsync(); // process data }

Syntactically, the only difference from async Task is the return type. But the runtime behavior differs in two important ways. First, an async void method cannot be awaited by the caller. The caller receives no Task to track completion or observe exceptions. Second, exceptions thrown inside an async void method are not captured by the caller; they are raised on the SynchronizationContext that was current when the method started.

The Exception Problem: Unhandled Exceptions Crash the Process

Consider this code:

public async void Process() { await Task.Delay(100); throw new InvalidOperationException("Failed"); }

When Process is called, it starts executing synchronously until the first await. The exception is thrown after the await resumes, typically on a thread pool thread or the UI thread depending on the context. Because the method returns void, there is no Task to carry the exception. The runtime rethrows it on the current SynchronizationContext, and if no handler catches it, the process terminates.

In a console application, this means an unhandled exception crashes the process. In ASP.NET Core, it can bring down the entire request pipeline. In a desktop UI application, it often triggers the Application.ThreadException event, but if that event is not handled, the application closes. This is the core reason async void is discouraged for anything except event handlers.

Why async void Cannot Be Awaited

Because async void returns void, the caller has no Task to await. This has practical consequences:

// This does not compile: cannot await void await Process();

You cannot use await on a method that returns void. You also cannot catch exceptions from the caller's perspective. The only way to observe an exception is to install a global handler like AppDomain.UnhandledException or TaskScheduler.UnobservedTaskException—but those are unreliable and often too late.

The inability to await also means you cannot easily sequence operations. If you need to start an async void method and then perform work after it completes, you have no built-in mechanism to do so. This makes correctness harder to guarantee.

When async void Is Acceptable: Event Handlers

Event handlers are the one place where async void is the standard and recommended approach. The .NET event model expects a void return type, so you cannot use async Task directly. For example, a button click handler in a desktop app:

private async void OnButtonClick(object sender, EventArgs e) { var result = await LoadDataAsync(); UpdateUI(result); }

Here, the event handler returns void because that is what the delegate requires. The UI framework captures the SynchronizationContext and uses it to resume the method on the UI thread after the await. This is safe because the event handler is the top-level entry point; exceptions are routed to the framework's error handling mechanism, which typically shows a dialog or logs the error without crashing the process immediately.

Still, you should handle exceptions inside the handler rather than relying on the framework. An unhandled exception in an event handler can still leave your application in an inconsistent state.

Handling Exceptions in async void Event Handlers

Wrap the body of an async void event handler in a try-catch block to prevent exceptions from escaping:

private async void OnButtonClick(object sender, EventArgs e) { try { var result = await LoadDataAsync(); UpdateUI(result); } catch (Exception ex) { // Log the exception and show a user-friendly message MessageBox.Show($"Error: {ex.Message}"); } }

This ensures the exception is handled on the UI thread, and the application continues running. Never leave an async void method without a try-catch if it can throw. The same rule applies to any async void method, not just event handlers.

Prefer async Task for Non-Event Code

For any method that is not an event handler, use async Task or async Task<T> instead. This gives the caller the ability to await, catch exceptions, and control flow:

public async Task LoadDataAsync() { var data = await FetchDataAsync(); // process data } // Caller can await and handle errors public async Task CallerAsync() { try { await LoadDataAsync(); } catch (Exception ex) { // Handle exception } }

This pattern is safer and more flexible. If you need to call an async Task method from a non-async context, you can use await in an async method or use .GetAwaiter().GetResult() in a synchronous method—though that can cause deadlocks in UI contexts if the SynchronizationContext is blocked.

Practical Guidance for Fire-and-Forget Scenarios

Sometimes you need to start an asynchronous operation without waiting for it. This is often called fire-and-forget. Avoid using async void for this purpose. Instead, use async Task and explicitly handle exceptions:

public async Task FireAndForgetAsync() { try { await DoWorkAsync(); } catch (Exception ex) { // Log the exception, but do not rethrow LogError(ex); } } // Caller ignores the task, but exceptions are handled internally _ = FireAndForgetAsync();

By returning Task, you retain the ability to observe completion or errors if you later decide to await it. The _ = discard simply suppresses the compiler warning about an unawaited task. The method itself handles its own exceptions, so the process does not crash.

If you must use async void outside an event handler, you are responsible for every exception. The safest approach is to wrap the entire method body in a try-catch and never let an exception escape. But even then, you lose the ability to know when the method finishes, which can lead to race conditions and resource leaks.

In summary, c# async void is a narrow tool designed for event handlers. For all other asynchronous code, prefer async Task. When you do use async void, always handle exceptions locally and never assume the caller can observe them. The exception behavior is the key difference, and it is the reason why async void is so often the source of production crashes.

c# async void: Exception Risks and Safe Usage | RYUSLOG DEV