async void method
| Vulnerability potential | Low |
| DDoS potential | Medium |
async void cannot be awaited and crashes the process on unhandled exceptions; use async Task instead (except for event handlers)
Impact
An async void method returns void instead of Task, so the compiler-generated state machine has nothing to hand back to the caller. The caller cannot await it, cannot know when it finishes, and cannot observe whether it succeeded. This produces three concrete failures:
- Process crash on unhandled exception. When an exception escapes an
async voidmethod, there is noTaskto carry it. The runtime rethrows it on the capturedSynchronizationContext(or, with no context, on a thread-pool thread) as an unobserved exception. Notry/catcharound the call site can catch it, because the call returned long before the exception was raised. The result is an unhandled exception that tears down the process. - Fire-and-forget races. Because completion is invisible, subsequent code runs while the
async voidwork is still in flight, producing ordering bugs, use of half-initialized state, and disposal of objects still in use. - Untestable. A test cannot
awaitthe method, so it has no reliable way to wait for completion or to assert on thrown exceptions.
The only legitimate use is an event handler whose delegate signature requires void (e.g. EventHandler).
Vulnerability potential
The dominant risk is availability rather than confidentiality or integrity.
- Denial of service via forced crash. If an
async voidmethod can throw on attacker-influenced input (a malformed request, an oversized payload, a parse failure), the attacker can reliably crash the hosting process. Unlike a faulting request handler that returns500, this exception bypasses the normal exception pipeline and terminates the process, taking down all in-flight requests with it. Repeated triggering is a cheap, reliable DoS. - Swallowed security-relevant failures. Fire-and-forget
async voidwork (audit logging, token revocation, cache invalidation) can fail silently because nobody observes its result, leaving the system in a state the caller assumes was reached.
There is no direct memory-safety or injection angle; the security relevance is confined to availability and silent failure of background work.
Technical details
Why the exception escapes
For an async Task method, the compiler builds a state machine driven by AsyncTaskMethodBuilder. When the body throws, the builder calls SetException, which stores the exception on the returned Task. The exception is dormant until someone awaits, calls .Result/.Wait(), or the Task is finalized — at which point it surfaces in a controlled way.
An async void method is instead driven by AsyncVoidMethodBuilder. There is no Task, so SetException cannot park the exception anywhere observable. Its implementation captures the SynchronizationContext present when the method started and posts the exception to it via Post, which rethrows on that context. If there is no context (typical for console apps, thread-pool callbacks, and most server frameworks), it is rethrown directly on a thread-pool thread. Either way it becomes an unhandled exception on a thread that the original caller does not control, so AppDomain.UnhandledException fires and the CLR terminates the process.
Completion is unobservable
AsyncVoidMethodBuilder exposes no awaitable. The caller’s stack unwinds at the first incomplete await inside the method, returning control immediately while the continuation is still scheduled. There is no handle to join on, which is the root of both the race conditions and the untestability.
The fix is simply to return Task (or Task<T>): async Task DoWorkAsync(). The caller can then await it, exceptions land on the Task, and tests can synchronize on completion.
Catching the issue
Roslyn / analyzers
- VSTHRD100 (
Microsoft.VisualStudio.Threading.Analyzers) — “Avoidasync voidmethods” — flags everyasync voidthat is not an event handler. - VSTHRD101 — warns about
async voidlambdas, a common hidden form. - The .NET SDK analyzer CA2007/AsyncFixer family and AsyncFixer03 (“Fire-and-forget async void methods”) also report it.
SonarQube
- S3168 — “
asyncmethods should not returnvoid” — exactly this rule, with the event-handler exception built in.
Code review
Grep for async void. Allow it only where the signature is mandated by a delegate type (event handlers, EventHandler/EventHandler<T>). In those handlers, wrap the entire body in try/catch so no exception escapes, since the handler itself is still an async void. Everywhere else require async Task.
How to reproduce
The try/catch at the call site does not catch the exception; the process terminates with an unhandled exception.
using System;
using System.Threading.Tasks;
class Program
{
static async void FireAndForget()
{
await Task.Delay(100);
throw new InvalidOperationException("boom");
}
static async Task Main()
{
try
{
FireAndForget(); // returns immediately; nothing to await
}
catch (Exception ex)
{
// Never reached: the method already returned before throwing.
Console.WriteLine($"caught: {ex.Message}");
}
await Task.Delay(500); // give the async void time to throw
Console.WriteLine("still alive?"); // never printed: process crashed
}
}