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.