Click here to Skip to main content
16,019,976 members
Articles / Programming Languages / C#

Async EventHandlers – A Simple Safety Net to the Rescue

Rate me:
Please Sign up or sign in to vote.
5.00/5 (6 votes)
14 Feb 2023CPOL6 min read 10.6K   21   8
A simple solution that you can implement to help improve your experience with async void event handlers, especially when it comes to exception handling

When we discuss async EventHandlers, the first thing that comes to mind for many of us is that it’s the only exception that we seem to allow for the dreaded async void setup. When I had written about this before, I was excited that I was exploring a solution that involved actually allowing async void to exist (without wanting to pull the rest of my hair out). For me, this was much more about some clever tricks we can use to overcome async EventHandlers than it was to provide solutions for avoiding the problem entirely.

With that said though, there was a lot of traction on the article, which I am very thankful for, and some folks expressed opinions that they’d rather solve async EventHandlers a different way. I thought this was a great point, so I wanted to come up with an alternative approach that doesn’t fix async void, but it allows you to a-void it (see what I did there?) entirely while solving some of the challenges with async EventHandlers.

In this article, I will present another solution that you can try out in your own code. We’ll address the pros and cons from my perspective with respect to how it can be used so you can decide if it makes sense for your use case. You can also find some interactable code on .NET fiddle right over here. Otherwise, you can check the code out on GitHub if you’d like to clone it down locally to try it out.

A Companion Video!

The Problem

The problem we face with async EventHandlers is that the signature for events that we can subscribe to in C# by default looks something like this:

C#
void TheObject_TheEvent(object sender, EventArgs e);

And you’ll notice that by having void out the front of this signature, we’re forced to use void in our own handlers in order to subscribe to the event. This means that if you want your handler to ever run async/await code, you’ll need to await inside your void method… which introduces the big scary async void pattern that we’re told to avoid like the plague.

And why? Because async void breaks the ability for exceptions to bubble up properly and can cause a ton of headaches as a result.

The previous article addressed this by allowing us to get creative on the invocation side of things but…

  • We might need support for this on objects we don’t control the invocation of events for (i.e., you are hooking up to a button’s click event in your favorite UI framework)
  • Some people see the usage of the context inside of that solution as a hack (I’m not disagreeing with that either).
  • … Specifically with event handlers, we have some other more simple tricks we can do to support async EventHandlers!

In my opinion, simple is better… so if you read my previous article on async void and your goal was really just to deal with EventHandlers, this should help.

Solving async EventHandlers with try/catch

Based on the conditions previously stated, the exception handling breaks down over the boundary of async void. If you have an exception that needs to bubble up crossing this boundary, then you’re going to be in for a fun time. And by fun, I mean if you enjoy debugging why stuff isn’t working and you don’t have a clear indication as to what’s breaking, then you’ll really have a great time.

So what’s the easiest way to fix this?

Let’s prevent exceptions from being able to cross this boundary in the first place using a simple tool we have access to: try/catch.

C#
objectThatRaisesEvent.TheEvent += async (s, e) =>
{
    // if the try catch surrounds EVERYTHING in the handler, no exception can bubble up
    try
    {
        await SomeTaskYouWantToAwait();
    }
    catch (Exception ex)
    {
        // TODO: put your exception handling stuff here
    }

    // no exception can escape here if the try/catch surrounds 
    // the entire handler body
}

As noted in the code above, if you place a try/catch block around the ENTIRE body of your event handler, then you can prevent any exceptions from bubbling up across that async void boundary. On the surface, it’s quite simple and doesn’t require anything fancy to implement this.

Pros

  • Extremely simple. No complex mechanisms to understand.
  • No packages required.
  • You don’t need to be the owner of the class that raises the event for this to work. This means that this approach will work for all existing event-raising objects including WinForms and WPF UI components.

Cons

  • You need to remember to do this… everywhere.
  • It’s possible that as your code evolves over time, someone might accidentally write logic outside of the event handler’s try catch that can throw exceptions

With that said, this solution truly is simple, but I think we can do a little bit better.

A (Slightly) Fancier Approach to Improving async EventHandlers

One improvement that I think we can make over the initially proposed solution is that we can make it a little bit more explicit that we have an async EventHandler that should be safe from bubbling up exceptions. This approach will also prevent code drift over time from causing problematic code from running outside of the event handler. However, it will not address the fact that you need to remember do add this in manually!

Let’s check out the code:

C#
static class EventHandlers
{
    public static EventHandler<TArgs> TryAsync<TArgs>(
        Func<object, TArgs, Task> callback,
        Action<Exception> errorHandler)
        where TArgs : EventArgs
		=> TryAsync<TArgs>(
            callback,
            ex =>
            {
                errorHandler.Invoke(ex);
                return Task.CompletedTask;
            });
    
    public static EventHandler<TArgs> TryAsync<TArgs>(
        Func<object, TArgs, Task> callback,
        Func<Exception, Task> errorHandler)
        where TArgs : EventArgs
    {
        return new EventHandler<TArgs>(async (object s, TArgs e) =>
        {
	    try
            {
                await callback.Invoke(s, e);
            }
            catch (Exception ex)
            {
                await errorHandler.Invoke(ex);
            }
        });
    }
}

The code above quite literally uses the exact same approach for preventing exceptions from crossing the async void boundary. We simply try catch around the body of the event handler, but now we’ve bundled it up into an explicit dedicated method to reuse.

Here’s how it would look to apply it:

C#
someEventRaisingObject.TheEvent += EventHandlers.TryAsync<EventArgs>(
    async (s, e) =>
    {
        Console.WriteLine("Starting the event handler...");
    	await SomeTaskToAwait();
        Console.WriteLine("Event handler completed.");
    },
    ex => Console.WriteLine($"[TryAsync Error Callback] 
          Our exception handler caught: {ex}"));

We can see that we now have a delegate with an async Task signature to work with, and anything we put inside of that we rest assured will have a try/catch around it within the helper method we saw earlier.

Here’s a screenshot showing the error handler callback properly capturing the exception:

Image 1

Pros

  • Still very simple. Wrapper function is *slightly* more complex, but still very basic.
  • No packages required.
  • You don’t need to be the owner of the class that raises the event for this to work. This means that this approach will work for all existing event-raising objects including WinForms and WPF UI components.
  • The intention is more obvious for working with async EventHandlers because of the syntax when hooking up the handler to the event.
  • Code drift that eventually throws more exceptions will still be wrapped inside the try/catch.

Cons

  • You still need to remember to hook this thing up!

Closing Thoughts on async EventHandlers

While originally I set out to explore interesting ways to deal with async void, the reader feedback was valid in that the examples focused on async EventHandlers and there surely must be a more simple way. In this article, we explored what I might argue is the most simple way to make your async EventHandlers behave properly and the refined solution (in my opinion) only has the drawback that you need to remember to use it.

A commenter had suggested that one could explore Aspect Oriented Programming (AoP) to inject this sort of behavior across your application so that you wouldn’t need to go remember to do it. There are some compile-time AoP frameworks that exist, but I’ll leave that as an exercise for you as the reader (because it’s also an exercise for me to go follow up on).

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Team Leader Microsoft
United States United States
I'm a software engineering professional with a decade of hands-on experience creating software and managing engineering teams. I graduated from the University of Waterloo in Honours Computer Engineering in 2012.

I started blogging at http://www.devleader.ca in order to share my experiences about leadership (especially in a startup environment) and development experience. Since then, I have been trying to create content on various platforms to be able to share information about programming and engineering leadership.

My Social:
YouTube: https://youtube.com/@DevLeader
TikTok: https://www.tiktok.com/@devleader
Blog: http://www.devleader.ca/
GitHub: https://github.com/ncosentino/
Twitch: https://www.twitch.tv/ncosentino
Twitter: https://twitter.com/DevLeaderCa
Facebook: https://www.facebook.com/DevLeaderCa
Instagram:
https://www.instagram.com/dev.leader
LinkedIn: https://www.linkedin.com/in/nickcosentino

Comments and Discussions

 
QuestionHow are these ideas different from using .ContinueWith( ) Pin
Dave Hary15-Feb-23 7:45
Dave Hary15-Feb-23 7:45 
AnswerRe: How are these ideas different from using .ContinueWith( ) Pin
Dev Leader15-Feb-23 10:09
mvaDev Leader15-Feb-23 10:09 
Hey thanks for taking the time to read and ask your question!

Conceptually I would say the goal here is accomplishing roughly the same thing, but the focus is on dealing with a situation that is historically forcing us to use "async void" (which is the real deal breaker). The "async void" situation brings on a world of pain once we have to start worrying about exceptions at that "async void" boundary. That's when you can have stuff silently breaking and uncaught in the app.

Not sure if it's helpful at all, but figured I'd link to a SO post that was discussing some of the differences with async/await vs ContinueWith just because there's some interesting nuances with aggregate exceptions etc...
https://stackoverflow.com/questions/18965200/difference-between-await-and-continuewith[^]

Side note, I think the IDE treats this as unhandled in your example because in the current context it's in (i.e. within that particular Task execution), it is unhandled. Kind of confusing because of course, you're right, eventually it is handled Smile | :)

But to summarize, what you are proposing is essentially what I am chasing except for situations where we have async void. A common spot that happens is event handlers (which is why I made this follow up article specifically addressing event handlers) because you're forced into async void syntax because of the default signature. In your example, you have async Tasks (not void) so you have the happy path, even if there are exceptions Smile | :)

Let me know if that helps answer. Otherwise, maybe you can clarify further and I can try again!
GeneralRe: How are these ideas different from using .ContinueWith( ) Pin
Dave Hary16-Feb-23 9:01
Dave Hary16-Feb-23 9:01 
GeneralRe: How are these ideas different from using .ContinueWith( ) Pin
Dev Leader16-Feb-23 15:41
mvaDev Leader16-Feb-23 15:41 
AnswerRe: How are these ideas different from using .ContinueWith( ) Pin
Dev Leader16-Feb-23 19:41
mvaDev Leader16-Feb-23 19:41 
GeneralRe: How are these ideas different from using .ContinueWith( ) Pin
Dave Hary18-Feb-23 10:59
Dave Hary18-Feb-23 10:59 
GeneralRe: How are these ideas different from using .ContinueWith( ) Pin
Dev Leader18-Feb-23 13:02
mvaDev Leader18-Feb-23 13:02 
GeneralThis article is a follow up from... Pin
Dev Leader14-Feb-23 12:42
mvaDev Leader14-Feb-23 12:42 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.