Why use "async void" in C#

Beginners are often told to avoid using async void in C#, and with good reason: 90% of the time, they actually wanted async Task. They’re told to only use async void in event handlers, where a Task can’t be returned.

However, async void does have some other uses. Take, for example, these two code snippets:

public void HandleIncomingEventBad(Event evt)
{
    // Do some initial processing
    // Finish off by pushing processing of the Event onto the ThreadPool
    Task.Run(() => ProcessEvent(evt));
}

public async void HandleIncomingEventGood(Event evt)
{
    // Do some initial processing
    // Finish off by pushing processing of the Event onto the ThreadPool
    await Task.Run(() => ProcessEvent(evt));
}

One the face of it, HandleIncomingEventBad and HandleIncomingEventGood are equivalent. They both do some synchronous processing, and then continue processing a ThreadPool thread. After handing off processing to the ThreadPool, they return. They don’t care when ProcessEvent completes.

This isn’t such an unusual pattern: something like it will crop up whenever you have a single producer which produces events at a high rate, and multiple slower consumers.

However, what happens if we make things go slightly wrong…

public void ProcessEvent(Event evt)
{
    throw new Exception("BOOM!");
}

Now can you spot the difference?

HandleIncomingEventBad will silently swallow the exception. Every trace of it will be lost.

HandleIncomingEventGood will re-throw the exception. It will be re-thrown on the ThreadPool, where (unless you take some specific action) it will crash your application.

Why? Tasks contain exception information, and Task.Run (and friends) use this: when Task.Run encounters an exception, that exception is caught and attached to a Task, and that Task is returned from Task.Run. If nothing else looks at that Task ever again, then the exception is lost. However, await (and Task.Wait() and Task.Result) will look to see if the Task being awaited contains an exception, and will re-throw that exception if it does.

(There is another mechanism: Task has a finalizer which, when run, will check to see if the Task has an exception which has never been looked at. If it does, then the event TaskScheduler.UnobservedTaskException is raised. However, this relies on the finalizer, and we all know you shouldn’t rely on the finalizer.)

In summary, never leave a Task hanging. If you call a method which returns a Task, always do something with it. Either return it, or await it, or call .Result or .Wait() on it, but do something with it: don’t just let it (and any exceptions it’s caught) disappear.