When writing async code, your method is allowed to return either void, Task, Task, or [ValueTask](https://www.vaughanreid.com/2020/06/when-to-consider-using-valuetask-over-task-with-async-code)). Since early on with async code, the advice has been to not use void return methods. But in real life there are times when you don't really have a choice. Since we can't always get away from using them, its good to understand whats the difference to async code returning a Task and what we can potentially do to help make it less *bad*.
To understand why async void is bad I’ll start with an example. I created a class AysncVoidTest which has a async void method that throws an exception. I call it from MyClass.DoSomething from within a try catch.
public class AysncVoidTest
{
public async void ThrowException(string input)
{
throw new Exception("something bad");
}
}
public class MyClass
{
AysncVoidTest _asyncVoidTest = new AysncVoidTest();
public void DoSomething()
{
try
{
asyncVoidTest.ThrowException("some input");
}
catch(Exception ex)
{
//handle exception
}
}
}
You may be mistaken for thinking that the try catch will do something in this case but it won’t. The simple reason is that the async keyword makes it run in another context but there is nothing waiting for the result. So the code execution will pass right over the try catch with nothing to get it back into the current context. This isn’t really what you want!
A common example where you will find an async void is if you want to fire your async method on response to an event. As you can imagine this can happen quite often when using third party APIs.
So what can you do? Well not much to be honest but at the least you can wrap your async code in something that will notify you if something went wrong. I created an example extension method called SafeFireAndForget that allows you execute an action if there is an exception. In some cases, this at least can give you visibility even if you can’t do much to handle the error.
public static class AsyncExtensions
{
public static async void SafeFireAndForget(this Task task, Action<Exception> onError = null)
{
try
{
await task;
}
catch(Exception ex)
{
onError(ex);
}
}
}
To show how this extension works, I created a class that fires an event. The first test Exception_in_event will pass without any indication that something went wrong. The second test Exception_in_event_with_safe_fire_and_forget will also pass but it will console out the exception.
public class HandleProducer
{
public delegate void HandleSomething(string input);
public event HandleSomething SomethingHappened;
public void Fire(string whatHappened)
{
SomethingHappened?.Invoke(whatHappened);
}
}
public class AysncVoidTest
{
public async Task ThrowExceptionAsync(string input)
{
throw new Exception("something bad");
}
}
[TestFixture]
public class AsyncVoidTests
{
public HandleProducer _producer = new HandleProducer();
[Test]
public void Exception_in_event()
{
var test = new AysncVoidTest();
_producer.SomethingHappened += async (input) => await test.ThrowExceptionAsync(input);
_producer.Fire("oh no");
Assert.Pass();
}
[Test]
public void Exception_in_event_with_safe_fire_and_forget()
{
var test = new AysncVoidTest();
_producer.SomethingHappened += (input) => test.ThrowExceptionAsync(input).SafeFireAndForget(e=>Console.WriteLine(e.Message));
_producer.Fire("oh no");
Assert.Pass();
}
}