I will say it once again: async await is awesome. As with every great tool, it is our responsability to understand how to use it for the best.
Async propagation
Let’s take an example of a Xamarin application were the user order a coffee by pressing a button in the application.
We obviously have a class who is responsible to prepare the coffee:
public class CoffeeService
{
public async Task PrepareCoffeeAsync()
{
// Asynchronously prepare an awesome coffee
await Task.Delay(2000);
}
}
As preparing a coffee takes time, and we don’t now how long it will take (because of possible incidents, strikes or the necessity of finding a new bag of coffee grain) this method is marked as async and returns a Task.
Any code that calls PrepareCoffeeAsync
will then need to await it’s completion.
The most obvious way to do this is to mark the caller as async like in the following view model:
public class CoffeeViewModel
{
public async Task PrepareCoffeeAsync()
{
IsBusy = true;
var coffeeService = new CoffeeService();
await coffeeService.PrepareCoffeeAsync();
IsBusy = false;
}
}
Repeat the same process enough and you will reach a point where you cannot change the return type to Task and you will face the async void.
Is async void that bad ?
I can summarize it like this:
- It generates compiler warnings
- If an exception is uncaught there, your application is dead
- You won’t probably have a proper call stack to debug with
- If your application crashes:
- Your users will not be happy
- Your boss will not be happy
- You will loose your job, and then your significant other…
Actually I don’t know about the last one but anyway, async void is a BAD guy.
There are tons of awesome articles likes ones from Stephen Cleary and Phil Haack (links below) that explain in details the impacts of async void and I very much encourage you to read them.
But I have no choice !
It is unfortunately true. At one point of the async propagation, you will reach a method where you cannot change the return type for example:
- Lifecycle method
- Event handler
- Delegate
- Lambda expressions
We end up with a code like this:
public async void OnPrepareButtonClick(object sender, EventArgs e)
{
Button button = (Button)sender;
button.IsEnabled = false;
activityIndicator.IsRunning = true;
var coffeeService = new CoffeeService();
await coffeeService.PrepareCoffeeAsync();
activityIndicator.IsRunning = false;
button.IsEnabled = true;
}
Removing async void
If you are just interested by a reusable piece of code to use in your project take a look at the AsyncAwaitBestPratices library that was inspired by the content of this post.
Studying the code
With a little bit of refactoring we can isolate async void methods in our code.
But first let’s study and annotate what is in the code:
- Casting into a button
Button button = (Button)sender;
- Showing the user we are starting an asynchronous operation
button.IsEnabled = false; activityIndicator.IsRunning = true;
- Run the async code
var coffeeService = new CoffeeService(); await coffeeService.PrepareCoffeeAsync();
- Showing the user we are finished
activityIndicator.IsRunning = false; button.IsEnabled = true;
Moving things around
In this case, we can move steps 2 through 4 into an async method.
public async void OnPrepareButtonClick(object sender, EventArgs e)
{
Button button = (Button)sender;
await PrepareCoffeeAsync(button);
}
public async Task PrepareCoffeeAsync(Button button)
{
button.IsEnabled = false;
activityIndicator.IsRunning = true;
var coffeeService = new CoffeeService();
await coffeeService.PrepareCoffeeAsync();
activityIndicator.IsRunning = false;
button.IsEnabled = true;
}
Our event handler now has only one await at the end of the method. This is what we need to proceed and make our code safer.
Removing async void
For the event handler, awaiting PrepareCoffeeAsync
is now useless.
Since there is no code after, there is no need for completion information or a result.
It is now the typical fire and forget method.
Therefore, we can remove the async:
public void OnPrepareButtonClick(object sender, EventArgs e)
{
Button button = (Button)sender;
PrepareCoffeeAsync(button);
}
We no longer have async void but we are not done, since no exceptions is handled !
Handling exceptions
With try catch blocks
Using try catch blocks for handling exceptions is of course possible:
public async Task PrepareCoffeeAsync(Button button)
{
try
{
button.IsEnabled = false;
activityIndicator.IsRunning = true;
var coffeeService = new CoffeeService();
await coffeeService.PrepareCoffeeAsync();
activityIndicator.IsRunning = false;
button.IsEnabled = true;
}
catch (Exception ex)
{
// Do something
System.Diagnostics.Debug.WriteLine(ex);
}
}
The problem with this approach is that it will generate a lot of code duplication since there are lots of places where async void are present in a typical code base.
With extensions
What we need now is some task’s extension method to handle exceptions that could replace all theses try catch blocks throughout the code.
There are lots of ways to do it and none is really better than others. It is always a matter of taste.
Introducing FireAndForgetSafeAsync and IErrorHandler
So let me introduce my favorite extension method ever:
public static class TaskUtilities
{
#pragma warning disable RECS0165 // Asynchronous methods should return a Task instead of void
public static async void FireAndForgetSafeAsync(this Task task, IErrorHandler handler = null)
#pragma warning restore RECS0165 // Asynchronous methods should return a Task instead of void
{
try
{
await task;
}
catch (Exception ex)
{
handler?.HandleError(ex);
}
}
}
The FireAndForgetSafeAsync
method basically wraps tasks into a try catch block. If an error occurs, it send the exception to an error handler with should implement the following interface:
public interface IErrorHandler
{
void HandleError(Exception ex);
}
Of course, setting the error handler to null is not something to do !
I usually like my view models to either:
- Have a reference on a
IErrorHandler
injected at runtime - Implement the interface directly
Finalizing the code
Now that we have a the previous two methods in place we can make our code safer:
public void OnPrepareButtonClick(object sender, EventArgs e)
{
IErrorHandler errorHandler = null; // Get an instance from somewhere
Button button = (Button)sender;
PrepareCoffeeAsync(button).FireAndForgetSafeAsync(errorHandler);
}
Conclusion
Applying the exact same steps through very complex code base has enabled my team and me to overcome lots of issues in Xamarin applications where the usage of async await is extensive.
Little by little, moving things around and using FireAndForgetSafeAsync
reduced the number of crashes as well as improving our error reporting accuracy.
In the end, what is important to remember is that, whatever means you use,
Just remove async void !
As always, please feel free to read my previous posts and to comment below, I will be more than happy to answer.
Comments