The perils of async void


We saw last time that async void is an odd beast, because it starts doing some work, and then returns as soon as it encounters an await, with the rest of the work taking place at some unknown point in the future.

Why would you possibly want to do that?

Usually it's because you have no choice. For example, you may be subscribing to an event, and the event delegate assumes a synchronous handler. You want to do asynchronous work in the handler, so you use async void so that your handler has the correct signature, but you can still await in the function.

The catch is that only the part of the function before the first await runs in the formal event handler. The rest runs after the formal event handler has returned. This is great if the event source doesn't have requirements about what must happen before the handler returns. For example, the Button.Click event lets you know that the user clicked the button, but it doesn't care when you finish processing. It's just a notification.

On the other hand, an event like Suspending assumes that when your event handler returns, it is okay to proceed with the suspend. But that may not be the case if your handler contains an await. The handler has not logically finished executing, but it did return from its handler, because the handler returned a Task which captures the continued execution of the function when the await completes.

Aha, but you can fix this by making the delegate return a Task, and the event source would await on the task before concluding that the handler is ready to proceed.

There are some problems with this plan, though.

One problem is that making the event delegate return a Task is that the handler might not need to do anything asynchronous, but you force it to return a task anyway. The natural expression of this results in a compiler warning:

// Warning CS1998: This async method lacks 'await'
// operators and will run synchronously.
async Task SuspendingHandler(object sender, SuspendingEventArgs e)
{
  // no await calls here
}

To work around this, you need to add return Task.CompletedTask; to the end of the function, so that it returns a task that has already completed.

A worse problem is that the return value from all but the last event handler is not used.

If the delegate invocation includes output parameters or a return value, their final value will come from the invocation of the last delegate in the list.

(If there is no event handler, then attempting to raise the event results in a null reference exception.)

So if there are multiple handlers, and each returns a Task, then only the last one counts.

Which doesn't seem all that useful.

The Windows Runtime developed a solution to this problem, known as the Deferral Pattern. The event arguments passed to the event handler includes a method called Get­Deferral(). This method returns a "deferral object" whose purpose in life is to keep the event handler "logically alive". When you Complete the deferral object, then that tells the event source that the event handler has logically completed, and the event source can proceed.

If your handler doesn't perform any awaits, then you don't need to worry about the deferral.

void SuspendingHandler(object sender, SuspendingEventArgs e)
{
  // no await calls here
}

If you do an await, you can take a deferral and complete it when you're done.

async void SuspendingHandler(object sender, SuspendingEventArgs e)
{
  var deferral = e.SuspendingOperation.GetDeferral();

  // Even though there is an await, the suspending handler
  // is logically still active because there is a deferral.
  await SomethingAsync();

  // Completing the deferral signals that the suspending
  // handler is logically complete.
  deferral.Complete();
}

The Suspending event is a bit strange for historical reasons.

Starting in Windows 10, there is a standard Deferral object which also supports IDisposable, so that you can use the using statement to complete the deferral automatically when control leaves the block. If the Suspending event were written today, you would be able to do this:

async void SuspendingHandler(object sender, SuspendingEventArgs e)
{
  using (e.GetDeferral()) {

    // Even though there is an await, the suspending handler
    // is logically still active because there is a deferral.
    await SomethingAsync();

 } // the deferral completes when code leaves the block
}

Alas, we don't yet have that time machine the Research division is working on, so the new using-based pattern works only for deferrals added in Windows 10. A using-friendly deferral will implement IDisposable. Fortunately, if you get it wrong and try to using a non-disposable deferral, the compiler will notice and report an error: "CS1674: type used in a using statement must be implicitly convertible to 'System.IDisposable'".

And that's the end of CLR We... no wait! CLR Week will continue into next week! What has the world come to!?

Comments (21)
  1. OldFart says:

    Yea! CLR Week Ex!

    1. camhusmj38 says:

      Surely, CLRWeeksEx

      1. Lel says:

        NtCLRWeekEx

    2. Vilx- says:

      await CLRWeek2();

    3. pc says:

      You mean a new value from CLRWeekFactoryManagerExecutionPolicyFactory.

      Sorry, my day job uses Java.

  2. pete.d says:

    Those Windows guys crack me up. I still remember years ago, providing feedback about the use of the DEVMODE structure by print drivers to store global data (like, entire font tables), rather than limiting it to document-specific data, resulting in file size bloat by program (like Excel) that saved the DEVMODE structure in the file.

    They took the feedback constructively, but unfortunately decided instead of just moving the global data somewhere else, bifurcated the whole DEVMODE-related part of the API into “DEVMODE with all that junk” and “DEVMODE without all that junk”, overcomplicating everyone’s lives.

    And now, presented with the need for an event that _knows_ about the possibility of asynchronous completion of handlers (a rare but valid scenario), they introduce this weird concept of a “deferral” for the object, when making the event handler return a Task would actually work just fine, and would be far more expressive and consistent with the TPL model.

    The presumed “problem” — “that the return value from all but the last event handler is not used” — is no problem at all. That statement is true only for naïve invocations of the delegate. It is in fact possible to observe _all_ of the return values, if one invokes each member of the event’s delegate’s invocation list separately. And indeed, that’s exactly what’s demonstrated in this Stack Overflow answer: https://stackoverflow.com/a/27763068/3538012 (yes, that’s me).

    Oh well…that’s Windows for you. Fighting every step of the way to undo the API simplification work the C# and .NET teams put so much effort into. “What? We’re going to have a whole new platform, covering mobile, desktop, and everything else? Well, we can’t possibly leverage all that work the .NET folks already did…we’ll have reinvent all those wheels!” Sigh…

    1. ChDF T says:

      Disclaimer: The following is based on speculation based on the assumption, that there was a reason to design the API in this way.

      While your proposal (return Tasks from event handlers) is a neat solution for the problem, the deferral object based approach has the advantage of not braking existing code and conventions.
      UWP was intended as successor of Windows 8-style-apps, so it seems critical to maintain source code compatibility. Changing the return type of most event handlers would have caused a lot of compiler errors when porting a project to UWP.
      Additionally raising a traditional event (returning void) involves less boiler plate code and as noted in a comment below, declaring an event using a delegate with a non-void return type is problematic in vb.net.

      1. pete.d says:

        Sorry, I don’t understand what you mean (and I don’t mean because “breaking” is misspelled).

        The deferral-based pattern works only with _new_ event types, because you still need the special event args type to use it. So there’s no “old code” to break. Any code using the deferral pattern is new code, and that new code can just as easily use a Task-based event handler as the new event args type.

        Concerns about raising the event and not being able to implement such events are similarly unworthy. Implementation of such events needs to be done just once; it’s simple to create a template or helper method that will do it, just as we used to have to do in C# for regular events before the ?. operator came along. Besides, it’s not like implementing the deferral approach is a cake-walk. In fact, I’d argue it’s _more_ complicated, due to the stateful nature and lack of compiler support for dealing with that state.

        And who cares if you can _implement_ such events in VB.NET? Should the world of .NET and Windows be dragged down just because of some arbitrary restriction found in one language? There aren’t going to be many scenarios where the event needs to be implemented, and for those, it’s reasonable enough to just say “you’ll have to do that in some language other than VB.NET”. I doubt there are many Windows components being implemented in VB.NET anyway, and if this pattern comes up in third-party components, all the better if people actually using VB.NET lobby to improve the language to remove that particular restriction.

        1. On the other hand, it means that the documentation for delegates would say “The return value of the delegate is the value returned by the last handler, unless the delegate is used for a WinRT event that returns a Task, in which case the tasks returned by the individual handlers are aggregated and the return value is a new Task that completes when all the aggregated tasks complete.” And good luck with C++ and JavaScript.

          1. pete.d says:

            “it means that the documentation for delegates would say”

            Sorry, why would it say that? The implementation of _delegates_ doesn’t have to change. It’s just the code that _uses_ delegates, for _those specific events_. And for those, so what? They are odd-ball events however you look at it. They either have to document the (IMHO definitely) awkward “deferral” design, or the (arguably not so) awkward “Task” design. One way or the other, there’s some boilerplate that’s going to have to go into the documentation, but it only has to be for the event, and using the “Task” design, the boilerplate doesn’t have to introduce any new concepts. It just leverages the existing and very usable TPL idioms.

          2. News flash: C++ and JavaScript don’t use TPL. WinRT is language-agnostic. In particular, JavaScript event handlers don’t return a value at all, so the discussion of the correct return type is moot.

  3. Bombel says:

    It is nice when you can modify type of arguments for handler, however, if you cannot do that, you can always make some nasty hacks: https://blog.adamfurmanek.pl/2017/01/14/async-wandering-part-4-awaiting-for-void-methods/
    Also, returning a Task from event handler is not available in VB.NET since it doesn’t allow return values.

    1. Jonathan Gilbert says:

      Huh, so if you use another CLR language to create a type with an event whose delegate type has a return value, that type literally cannot be consumed by VB.NET?

      1. Jonathan Gilbert says:

        I have just tested this and found it to be untrue.

    2. pete.d says:

      “returning a Task from event handler is not available in VB.NET since it doesn’t allow return values” — non sequitur. That is, while it’s true that an event _declared_ by a VB.NET type cannot use a delegate type with a return values (an arbitrary and unfortunate restriction in the VB.NET language, IMHO), a VB.NET event _handler_ certainly can return a Task object. Nothing in the language prohibits that.

      1. DWalker07 says:

        VB.NET has pretty good parity with C# .NET these days.

        1. pete.d says:

          “VB.NET has pretty good parity with C# .NET these days” — non sequitur. Yes, VB.NET has “pretty good parity” with C#. But that doesn’t change the fact that, in VB.NET, you can’t declare an event in a class where the delegate type for the event includes a non-void return type.

  4. Kevin Fee says:

    I’m having difficulty parsing the following sentence after the warning:
    // operators and will run synchronously.

    Operators and what?

    1. That’s not a sentence after the warning. It is a continuation of the warning text. Full text: “This async method lacks ‘await’ operators and will run synchronously.”

  5. David Haim says:

    Another reason to use “async void” when designing a function is that you simply don’t care about the return value of a function, nor an exception that may be thrown. This gives an optimization chance to the compiler because now there is no task object to allocate, set , lock, etc.

Comments are closed.

Skip to main content