Task EventHandlers - The Little Secret You Didn't Know

As C# programmers, we've all been burned by asynchronous EventHandlers. And if you still haven't yet, then hopefully this article will arm you for being able to better navigate long debugging sessions where functionality mysteriously stops working in your application. While there are several different solutions for dealing with async event handlers, either by avoiding use of async void or even by embracing async void, in this article we will explore another option which is Task EventHandlers.

Disclaimer: this article was originally written with the perspective that this solution feels close to bullet-proof, but there are important limitations. These limitations are addressed later in the article, and I felt it would still be valuable to explore the space (both positives and negatives).


A Companion Video!

https://www.youtube.com/watch?v=QVGjpRz0JZU
What you DIDN'T know about C# Async EventHandlers

The Source of the Problem: async void

Normal EventHandlers in C# have a void return type in their signature. This isn't an issue until we want to wire up an EventHandler that is marked as async because we'd like to await some Task that is called inside.

And why is that an issue? Because async void breaks the ability for exceptions to bubble up properly and can cause a debugging nightmare when you're unable to trace where your exceptions are flowing to.

Fundamentally, the complication we have is just because the signature of an EventHandler has a void return type and this breaks exception control:

void TheObject_TheEvent(object sender, EventArgs e);

But what if we could work around this?


Task EventHandlers Could Work!

Admittedly, the specific solution we're going to dive into has a more limited use case than some of the other solutions I have mentioned before. However, I still feel that it's a very viable solution when you have control over the events that you're adding to your classes and we understand the limitations. That is, if you are creating a class and defining your own events that you would like callers to be able to subscribe to, this solution may work for you. We'll go over another drawback afterwards that is important to understand and like all things, I think it's important to understand pros and cons before making decisions.

Based on what was said in the last section, the issue that we can try to sort out here is the void return type on EventHandlers. When we create our own events, we generally will declare them using the existing delegate signatures:

public event EventHandler<SomeEventArgs> MyEvent;

But again, this EventHandler signature has a void return type. So what if we made our own?

public delegate Task AsyncEventHandler<TArgs>(object sender, TArgs args)
where TArgs : EventArgs

You can see an example of a class using that here on GitHub or below:

public sealed class AsyncEventRaisingObject
{
    // you could in theory have your own event args here
    public event AsyncEventHandler<EventArgs> ExplicitAsyncEvent;

    public async Task RaiseAsync(EventArgs e)
    {
        await ExplicitAsyncEvent?.Invoke(this, e);
    }
}

Let's See An Example

Now let's look at a sample application that combines our delegate that we created as well as the class that we defined above. You can also find this code on GitHub:

Console.WriteLine("Starting the code example...");
var asyncEventRaisingObject= new AsyncEventRaisingObject();
asyncEventRaisingObject.ExplicitAsyncEvent += async (s, e) =>
{
    Console.WriteLine("Starting the event handler...");
    await TaskThatThrowsAsync();
    Console.WriteLine("Event handler completed.");
};

try
{
    Console.WriteLine("Raising our async event...");
    await asyncEventRaisingObject.RaiseAsync(EventArgs.Empty);
}
catch (Exception ex)
{
    Console.WriteLine($"Our exception handler caught: {ex}");
}

Console.WriteLine("Completed the code example.");

async Task TaskThatThrowsAsync()
{
    Console.WriteLine("Starting task that throws async...");
    throw new InvalidOperationException("This is our exception");
};

The code above will set us up with a Task EventHandler that will eventually throw an exception because of the Task that it awaits on. Given that we defined our event signature to be Task instead of void, this allowed us to have a Task EventHandler. And the result we can see below:

Console output for Task EventHandler where an exception is thrown.

The Catch

There's one big smelly catch with this implementation that unfortunately for many of my use cases makes it a deal breaker. However, there could be some creative workarounds depending on what you're trying to accomplish.

Events and EventHandlers don't *quite* operate just like a callback. The +/- syntax that we get from them allows us to add and remove handlers essentially to an invocation list. Task EventHandlers break down where the exception is thrown in an earlier executed handler and we have a handler that follows. If we reverse the order and the Task EventHandler that throws the exception is at the end of the invocation, we will get the behavior that we demonstrated in the previous section. Given that this behavior might feel wildly inconsistent to a subscriber of your events, this puts us in a pickle.

While I don't necessarily suggest this, I think depending on your use case you may consider the following scenario. You could add custom add/remove event syntax if your design only required one handler for the object lifetime. That is, during the add overload, you could check if not null and only allow registration in that case.

An alternative might be exploring something like the following:

public event AsyncEventHandler<EventArgs> ExplicitAsyncEvent
{
    add
    {
        _explicitAsyncEvent += async (s, e) =>
        {
            try
            {
                await value(s, e);
            }
            catch (Exception ex)
            {
                // TODO: do something with this exception?
                await Task.FromException(ex);
            }
        };
    }

    // FIXME: this needs some thought because the 
    // anonymous method signature we used for try/catch
    // support breaks this
    remove { _ExplicitAsyncEvent -= value; }
}

In the above code, we actually use the trick from this article where we wrap our handlers in a try/catch. Layering in the try/catch this way creates other complexity around what you intend to do with the exception and unhooking events gets more complicated as well.


Task EventHandlers... Worth It?

To conclude, in this article we looked at an alternative approach where you can completely avoid using async void. The catch for this solution is that you have control over the class that is defining the events to subscribe to and you understand the multiple handler challenges. This solution will not work if you are trying to solve exception handling for async event handlers for classes that already have these events defined. For that, you may want to check out this article or this article.

If I found myself in a situation where I wanted to use an event instead of a callback and could guarantee a single subscriber, this might be a viable option. However, given that many of the systems I design would likely want to support N subscribers, Task EventHandlers may not be viable for much of what I write.

async void - How to Tame the Asynchronous Nightmare

Most intermediate dotnet devs writing async await code in C# will come across async void at some point. Here's a creative solution for avoiding the headaches.

Async EventHandlers - A Simple Safety Net to the Rescue

Dealing with async EventHandlers in C# can be very problematic. async void is a pattern cause headaches with exceptions. Check out this simple solution!

Async Event Handlers in C#: What You Need to Know

Learn how to safely use async event handlers in C#. Understand the dangers and discover best practices for managing async event handlers in your C# code.

An error has occurred. This application may no longer respond until reloaded. Reload x