- Item 27: Use Async Methods for Async Work
- Item 28: Never Write async void Methods
- Item 29: Avoid Composing Synchronous and Asynchronous Methods
- Item 30: Use Async Methods to Avoid Thread Allocations and Context Switches
- Item 31: Avoid Marshalling Context Unnecessarily
- Item 32: Compose Asynchronous Work Using Task Objects
- Item 33: Consider Implementing the Task Cancellation Protocol
- Item 34: Cache Generalized Async Return Types
Item 28: Never Write async void Methods
The title of this item makes a strong assertion, and there are a small number of exceptions to its advice (as you’ll see in this item). Nevertheless, this advice is stated forcefully because it is so important. When you write an async void method, you defeat the protocol that enables exceptions thrown by async methods to be caught by the methods that started the asynchronous work. Asynchronous methods report exceptions through the Task object. When an exception is thrown, the Task enters the faulted state. When you await a faulted task, the await expression throws the exception. When you await a task that faults later, the exception is thrown when the method is scheduled to resume.
In contrast, async void methods cannot be awaited. There’s no way to for the code that calls an async void method to catch or propagate an exception thrown from the async method. Don’t write async void methods because errors are hidden from callers.
Code in an async void method may generate an exception. Something must happen with those exceptions. The code generated for async void methods throws any exceptions directly on the SynchronizationContext (see Item 27) that was active when the async void method started. That makes it much more difficult for developers using your library to process those exceptions. You must use the AppDomain.UnhandledException or some similar out-of-band catch-all handler. Note that AppDomain.UnhandledException does not enable you to recover from the exception. You can log it, and possibly save data, but you cannot prevent the uncaught exception from terminating the application.
Consider this method:
private static async void FireAndForget()
{
var task = DoAsyncThings();
await task;
var task2 = ContinueWork();
await task2;
}
If you wanted to log errors before calling FireAndForget(), you would need to set up the unhandled exception handler. This example writes the exception information to the console in the Cyan color:
AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine(e.ExceptionObject.ToString());
};
There’s really no need to set the console’s ForegroundColor back to its original color, as the application will terminate.
Forcing developers to use an error-handling mechanism that is completely different from the one they use in all of their other code is bad API design. Many developers, of course, will neglect to do this extra work. It’s even worse to give developers no way to recover from any errors. If developers don’t do the extra work, any exceptions generated from async void methods will be unreported. The runtime will still abort the thread running in the synchronization context, but developers using your code will get no notifications, no catch handlers will be triggered, and no exception logging will happen. In essence, the thread just silently goes away.
In addition to the exception behavior, async void methods bring up other problems. In many async methods, you’ll want to start asynchronous work, await that task, and then do more work after the first awaited task has finished. It is easy to create the async tasks in such cases. However, as noted earlier, async void methods cannot be awaited. Therefore, developers using your async methods cannot readily determine when an async void method has finished all its work. That means easy composition is no longer possible. An async void method is essentially a “fire and forget” method: Developers start asynchronous work, but do not and cannot easily know when that work finishes.
These same issues complicate the process of testing async methods. Automated tests cannot know when an async void method has completed. Therefore, the automated test cannot be written to check for any effects from the async void method running to completion. Consider writing an automated unit test for this method:
public async void SetSessionState()
{
var config = await ReadConfigFromNetwork();
this.CurrentUser = config.User;
}
To write a test, you might consider code like the following:
var t = new SessionManager();
t.SetSessionState();
// Wait a while
await Task.Delay(1000);
Assert.Equal(t.User, "TestLibrary User");
There are bad practices here, and in fact, they may not always work. The key is the Task.Delay call. You can’t write this test resiliently, because you don’t know when the asynchronous work ends. Maybe 1 second is enough—but maybe not. Even worse, perhaps 1 second is sufficient in most cases, but on rare events, it is not. In this kind of scenario, your tests will fail and provide false feedback.
It should be clear by now that async void methods are bad. Whenever possible, you should create async methods that return Task objects or other awaitable objects (see Item 34). Nevertheless, async void methods are allowed because without these methods, you cannot create async event handlers.
The protocol for event handlers, in which event handlers are void returning methods, was established before async and await support was added to the C# language. Even if changes were made, you would still need async void methods to attach async event handlers to events that had been defined in earlier versions. In addition, the library author may not know whether an event handler requires asynchronous access. Taking all these points into consideration, the C# language supports void returning async methods. Also, callers of event handlers are typically not user code. If the caller won’t know what to do with a returned Task, why require this object to be returned?
Even though this item’s title says that you should never write async void methods, one day you’ll undoubtedly find that you must write an async void event handler. If you must do so, you should write the async event handler as safely as possible.
To achieve this goal, start by recognizing that async void methods can’t be awaited. The code that raised the event won’t know when your event handler has finished running. Event handlers typically do not return data to the caller, so the caller can “fire and forget” when raising events.
Handling any potential exceptions safely requires more work. If any exceptions are thrown from your async void method, the synchronization context will be killed. You must write your async void event handler so that no exceptions are thrown from this method. This may go against other recommendations, but this is one idiom where you usually want to catch all exceptions. The pattern for a typical async void event handler becomes something like this:
private async void OnCommand(object sender, RoutedEventArgs e)
{
var viewModel = (DataContext as SampleViewModel);
try
{
await viewModel.Update();
}
catch (Exception ex)
{
viewModel.Messages.Add(ex.ToString());
}
}
This code assumes you feel safe simply logging any exceptions and continuing normal execution. In fact, this kind of behavior may be safe in many scenarios. If that’s true for your scenario, you’re done.
But what if some exceptions that might be thrown in this event handler are catastrophic conditions that can’t be handled? Perhaps they can cause serious data corruption. In such a case, you will want to terminate the program immediately, rather than continue blithely along and further corrupt data. To achieve this outcome, you’ll want to throw the exception and have the system abort the thread on that synchronization context.
As part of this process, you’ll likely want to log everything, and throw the exception from the async void method. That’s a slight modification to the earlier code:
private async void OnCommand(object sender, RoutedEventArgs e)
{
var viewModel = (DataContext as SampleViewModel);
try
{
await viewModel.Update();
}
catch (Exception ex) when (logMessage(viewModel, ex))
{
}
}
private bool logMessage(SampleViewModel viewModel,
Exception ex)
{
viewModel.Messages.Add(ex.ToString());
return false;
}
This method logs every exception by using an exception filter (see Effective C#, Third Edition, Item 50) to log information about the exception. It then rethrows the exception to cause the synchronization context to stop execution, possibly stopping the program as well.
Both of these methods can be generalized by using a Func argument to represent the asynchronous work performed in each of these methods. You can then reuse the common elements from these two idioms.
public static class Utilities
{
public static async void FireAndForget(this Task,
Action<Exception> onErrors)
{
try
{
await task;
}
catch (Exception ex)
{
onErrors(ex);
}
}
public static async void FireAndForget(this Task task,
Func<Exception, bool> onError)
{
try
{
await task;
}
catch (Exception ex) when (onError(ex))
{
}
}
}
In the real world, the best solution may not always be as simple as catching all exceptions or rethrowing all exceptions. In many real-world applications, you may be able to recover from some exceptions but not others. For example, you may be able to recover from a FileNotFoundException, but no other exceptions. This behavior can be made more general and reusable by replacing the specific exception type with a generic type:
public static async void FireAndForget<TException>
(this Task task,
Action<TException> recovery,
Func<Exception, bool> onError)
where TException : Exception
{
try
{
await task;
}
// Relies on onError() logging method
// always returning false:
catch (Exception ex) when (onError(ex))
{
}
catch (TException ex2)
{
recovery(ex2);
}
}
You can extend that same technique to more exception types if you like.
These techniques help make async void methods a little more robust in terms of error recovery. They won’t help with testability or composability. In fact, there aren’t good techniques available to resolve those issues. That point is why you should limit the use of async void methods to the locations where you must write them: in event handlers. Everywhere else, never write async void methods.