As mentioned in the previous guide in this series, async
methods in C# are often spoken of as being either async Task
or async void
, referencing the two main return types. Generally, you are going to want to stick with the former by returning Task
, but there is also an important place for async
methods that return void
. It may not always be easy to identify when that is appropriate, however. Fortunately, there are some guidelines that can help with this decision.
It's difficult to overemphasize the fact that, the vast majority of the time, returning Task
is the right choice when it comes to deciding the return type of an async
method. But unless the Main
entry point of your application is itself async
(which is supported starting with C# 7.1), at some point you are going to need to have an async
method that returns void
. This is in part due to the fact that async
methods that return Task
are "contagious", such that their calling methods' often must also become async
. Returning void
from a calling method can, therefore, be a way of isolating the contagion, as it were.
In this lies a danger, however. Imagine you have an existing synchronous method that is called in many different places of your application, and as you are modifying that method you find the need to await
something asynchronously. So you add the await
call and apply async Task
to the method signature, but then find that there are now compilation errors at every spot where the method is called. Feeling loathe to examine and modify each calling method, you try changing async Task
to async void
and recompile. Let's say that this time the application compiles. Great, right?
Well, probably not. There are a number of valid reasons to return void
from an async
method, but "it's easier" and "it lets my application compile" are not on that list. Remember, changing the return type has implications with regard to control flow. It turns out that the valid reasons for returning void
from an async
method are very limited in scope.
Without further ado, here are the valid reasons for returning void
from an async
method:
ICommand.Execute
. The ICommand
interface is part of a pattern used by some C# developers to represent events at the "view model" level, so its Execute
method is logically the same thing as an event handler. Unless you work with the architecture pattern known as MVVM, you may not encounter commands.
async/await
do not need to use or implement callbacks, but occasionally you may need to work with a library that does not make use of async/await
and instead requires you to provide a callback method. Unless the library expects an asynchronous callback, you will need to provide one with a synchronous signature, which for async
methods is only possible by returning void
.In short, if your async
method is an event handler or a callback, it's ok to return void
. What these have in common is that they are reactions to some kind of signal.
To better understand why event handlers (or any sort of signal reaction) are in their own category, imagine an application with a button. Let's say the button launches a process that creates something; maybe it uses an algorithm to generate an image of fractal art with random inputs. This is computationally expensive, so on a slow device, it could take some time. You, as the application developer, may want to allow the user to generate multiple images concurrently by clicking the button multiple times in a row. The button click just sends a signal that says "generate an image"; it does not need to wait for the first image to be generated before signaling that a second one is desired.
Naturally then, event handlers are not awaited; the signal is merely sent, and your event handler or callback method becomes a starting point. Another way to look at it is that event handler methods are generally not called explicitly, and if they are, the caller of your method is not interested in the outcome. Since returning a Task
from an async
method is only useful if the Task
is referenced in some way (usually implicitly via await
), returning a Task
from an event handler or callback method would serve no purpose. For this reason, and also as a general convention, it is appropriate to have async
event handler and callback methods return void
.
You may find that you sometimes need to define a method that is not an event handler or callback but is similar in that callers of the method do not need to concern themselves with the outcome. For example, maybe you want to post an API call to a remote server that collects analytics. You don't need a progress bar, and the user doesn't need to know when it finished, or even if it failed. This kind of thing can be called "fire and forget", and sometimes developers use an async
method that returns void
for that. To continue our earlier example, let's say a "Generate" button click should also attempt to log that click to a remote server.
1void Initialize()
2{
3 generateButton.OnClicked += OnGenerateButtonClicked;
4}
5
6void OnGenerateButtonClicked()
7{
8 PostAnalyticAction("generate button clicked");
9 GenerateImage();
10}
11
12async void PostAnalyticAction(string actionName)
13{
14 await new HttpClient().PostAsync("https://...", new StringContent(actionName));
15}
In the above example, PostAnalyticAction
is the "fire and forget" method. This method is purposefully not being awaited, so that execution of OnGenerateButtonClicked
will continue before PostAnalyticAction
has completed. This allows generation of the image to begin before a response is received from the remote analytics server.
While such a "fire and forget" approach may work fine in many cases, it is discouraged. Remember that, most of the time, the right choice is to return Task
from async
methods, unless it is an event handler or a callback. In this example, PostAnalyticAction
is neither, and is called explicitly rather than implicitly, which suggests that the caller (OnGenerateButtonClicked
) may be interested in the outcome (including any exceptions that may arise therein).
A better approach would be to return Task
from PostAnalyticAction
and await
it in OnGenerateButtonClicked
. This will require that OnGenerateButtonClicked
be marked as async
.
"But wait! Won't that mean that image generation will be delayed until a response is received from the remote analytics server?"
If that thought occurred to you, good job! You are right. We are no longer "forgetting" our method now; we are waiting for it to finish. We can "fix" that by making the analytics call the last thing we do. If there is no code after the await
, nothing is delayed by the wait time. Here is the updated code sample:
1async void OnGenerateButtonClicked()
2{
3 GenerateImage();
4 await PostAnalyticAction("generate button clicked");
5}
6
7async Task PostAnalyticAction(string actionName)
8{
9 await new HttpClient().PostAsync("https://...", new StringContent(actionName));
10}
Notice that now the only async void
method we have now is OnGenerateButtonClicked
, which is an event handler. Often you'll find that, with thoughtful refactoring, you can avoid the need for "fire and forget". If rearranging code doesn't do the trick, consider a loosely coupled approach, such that you are not calling the method in question directly. For example, you could use some kind of publish/subscribe messaging, or even a thread-safe collection, where you add to the collection synchronously, and a separate thread monitors for new items. Your first choice though should always be to use Task
in conjunction with await
.
Calling a method in a "fire and forget" manner is especially discouraged from within an
async
method that returnsTask
, since the implication of returningTask
is that it encompasses all of its behavior, including any additional childTask
instances that might be spawned.
Sometimes switching an existing method to async Task
can be extremely invasive to your code base. In such cases the "fire and forget" approach may be valid, at least as a temporary measure. If you really need to use "fire and forget", be sure to add an exception handler with logging of some sort in the async void
method. This will help you avoid crashes while ensuring you are aware of problems that could otherwise go undetected. Using our original "fire and forget" example as a starting point, it could be something as simple as the following:
1async void PostAnalyticAction(string actionName)
2{
3 try
4 {
5 await new HttpClient().PostAsync("https://...", new StringContent(actionName));
6 }
7 catch (Exception ex)
8 {
9 Console.WriteLine($"Exception while posting analytics: {ex}");
10 }
11}
You may also see the term "fire and forget" mentioned in reference to a similar approach where an
async
method returnsTask
, but theTask
is not awaited by the caller. That approach is likewise discouraged, but if you do use it, you would not usetry
/catch
as described above; logging of exceptions would be done by adding a continuation to theTask
in the caller as mentioned here.
Returning void
from an async
method is not difficult; in fact, in some cases it is easier than returning Task
. But it's useful to know that void
in the case of an async
method is intended for just a few specific cases. Try to let the async
"contagion" spread all the way up to your outer-most methods, be they event handlers, callbacks or the Main
application entry point. If possible, avoid "fire and forget" and stick to Task
in conjunction with await
. And if you do need to use "fire and forget", be sure to add the appropriate exception handling and logging. In following these guidelines, your usage of async
method return types will demonstrate mastery.