Skip to content

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Returning Void From a C# Async Method

Apr 11, 2019 • 11 Minute Read

Not a Trivial Topic

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.

When Does Returning Void Become a Consideration?

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.

Valid Reasons for Returning Void

Without further ado, here are the valid reasons for returning void from an async method:

  1. The method is an event handler. An event handler is a method that gets called when some expected "thing" or occurrence takes place. This could be a system-generated event, such as an expiration of some system imposed deadline, or it could be a user-generated event, such as a user clicking a button.
  2. The method is a "command", which usually means it is an implementation of 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.

  1. The method is a callback. A callback is a method that gets called upon completion of some asynchronous operation. In theory, C# developers working with 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.

Event Handlers Are Not Awaited

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.

Avoid Returning Void for Fire and Forget

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.

      void Initialize()
{
  generateButton.OnClicked += OnGenerateButtonClicked;
}

void OnGenerateButtonClicked()
{
  PostAnalyticAction("generate button clicked");
  GenerateImage();
}

async void PostAnalyticAction(string actionName)
{
  await new HttpClient().PostAsync("https://...", new StringContent(actionName));
}
    

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:

      async void OnGenerateButtonClicked()
{  
  GenerateImage();
  await PostAnalyticAction("generate button clicked");
}

async Task PostAnalyticAction(string actionName)
{
  await new HttpClient().PostAsync("https://...", new StringContent(actionName));
}
    

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 returns Task, since the implication of returning Task is that it encompasses all of its behavior, including any additional child Task instances that might be spawned.

If You Really Need Fire and Forget

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:

      async void PostAnalyticAction(string actionName)
{
  try
  {
    await new HttpClient().PostAsync("https://...", new StringContent(actionName));
  }
  catch (Exception ex)
  {
    Console.WriteLine($"Exception while posting analytics: {ex}");
  }
}
    

You may also see the term "fire and forget" mentioned in reference to a similar approach where an async method returns Task, but the Task is not awaited by the caller. That approach is likewise discouraged, but if you do use it, you would not use try/catch as described above; logging of exceptions would be done by adding a continuation to the Task in the caller as mentioned here.

In Conclusion

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.