-1

I need to subscribe for updates from 3rd party system using provided async-unfriendly library ( they will not support async in the near future). The simplified method for subscription expects delegate Action<float> onTemperatureChange:

// Third party library class WeatherServer { // I cannot change signature of this method (it is third party) void SubscribeTemperature(Action<float> onTemperatureChange); } 

I can easily work with such API using synchronous method:

void onTemperatureChange(float newTemperature) { // Synchronous temperature handling } weatherServer.SubscribeTemperature(onTemperatureChange); 

What is the recommended approach if I need to have asynchronous temperature handler like this:

async Task onTemperatureChangeAsync(float newTemperature) { var feelsLikeTemperature = await anotherLibrary1.GetFeelsLikeTemperature(newTemperature); await anotherLibrary1.WriteTemperature(feelsLikeTemperature); } 

Please note that above example is heavily simplified and internal handler logic is complex logic full of awaited async methods.

Considering I cannot change class WeatherServer and I have to live with subscription via Action<float> onTemperatureChange, I have explored following options:

  1. Change handler return type to async void async void onTemperatureChangeAsync(float newTemperature), but it complicates unit testing and exceptions:

    // async void async void onTemperatureChangeAsync(float newTemperature) { // asynchronous temperature handling (for example): var feelsLikeTemperature = await anotherLibrary1.GetFeelsLikeTemperature(newTemperature); await anotherLibrary1.WriteTemperature(feelsLikeTemperature); } 
  2. Change handler to sync method using Result on internal async call,but .Result() is a bad practice:

    void onTemperatureChangeAsync(float newTemperature) { // Result() is a bad practice anotherLibrary.WriteTemperature(newTemperature).Result(); } 

Since option 2 using .Result() can cause significant issues, I assume that I have to choose with async void.

Is there any other way ? I assume that I am not first one, who needs to solve such issue during migration to async world.

Edited: added more awaited calls to async void to demonstrate why await is needed inside handler (even if async void cannot be awaited) and removed wrong comment, that async void is completely bad practice as pointed by answers.

4
  • 1
    Wrapping handler's code into Task with Wait or GetAwaiter().GetResult() (not .Result, which is truly bad practice) has no benefits. You'll be able to put awaits inside it, but handler will still wait for its completion synchroniously. async void is not bad practice, it's just often used thoughtlessly where not supposed to be used. But event handlers (or delegates) is one of not so much cases, when it is applicable. Commented Oct 14, 2023 at 22:15
  • 1
    I mean, it is normally to make some Button.Click event handler as async void. But when you make async void connection to database followed by reading something with it - here comes a trouble. Commented Oct 14, 2023 at 22:24
  • 1
    Just ensure that your target object can handle potential concurrent incoming data/calls, and async void won't be a problem. Commented Oct 14, 2023 at 22:32
  • 1
    Related: Unit testing async void event handler. Commented Oct 16, 2023 at 4:34

1 Answer 1

0

async void is the way to go and it is the recommended solution for handlers. Refer to: https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming#avoid-async-void

Assuming that WeatherServer does not depend on subscriber's handler internal logic at all. In other words, that WeatherServer use fire and forget logic. Not relying on handler(s) finish their internal processing, before firing again. Additional info from @Stephen Cleary: "async void does allow overlapping event handlers, though (i.e., the next event can be triggered before the first event handler completes). If this is a problem, you can have the handler just insert the value into a queue (e.g., Channel), and then have a queue processor that dequeues the values one at a time and (asynchronously) processes each one."

Handler async void onTemperatureChangeAsync(float newTemperature) must follow some guidelines:

  • Should not raise/propagate exceptions. Exceptions thrown by async void methods cannot be caught by the methods that started the async void work. In other words, it is a good idea to handle any exception within handler onTemperatureChangeAsync.
  • Must be able to handle potential concurrent incoming calls from WeatherServer
  • Async void methods are difficult to test, so it might be good idea to implement proper async Task onTemperatureChangeAsync(float newTemperature) method which will be unit tested and async void wrapper to be used as handler
// Proper Async method (used for unit testing) async Task onTemperatureChangeAsync(float newTemperature) { try { var feelsLikeTemperature = await anotherLibrary1.GetFeelsLikeTemperature(newTemperature); await anotherLibrary1.WriteTemperature(feelsLikeTemperature); } catch(Exception exc) { log.Error("Handler failed"); } } // Handler async void onTemperatureChangeVoidAsync(float newTemperature) { await onTemperatureChangeAsync(newTemperature) } weatherServer.SubscribeTemperature(onTemperatureChangeVoidAsync); 

I have compiled the answer based on @Aditive hints/comments and https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming#avoid-async-void

Any further hints welcomed. I will try to add useful hints to this answer.

If WeatherServer should wait for handler before continuing or should even catch exceptions thrown by handler, the above recommendation to use async void does not apply, but it would be the edge case and it might be even a gap in the design…

Sign up to request clarification or add additional context in comments.

1 Comment

Yes, in this case async void is acceptable, since it is an event handler. Note that async void does allow overlapping event handlers, though (i.e., the next event can be triggered before the first event handler completes). If this is a problem, you can have the handler just insert the value into a queue (e.g., Channel<float>), and then have a queue processor that dequeues the values one at a time and (asynchronously) processes each one.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.