When we discuss async EventHandler
s, 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 EventHandler
s 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 EventHandler
s 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 EventHandler
s.
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 EventHandler
s is that the signature for events that we can subscribe to in C# by default looks something like this:
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 EventHandler
s!
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 EventHandler
s, 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
.
objectThatRaisesEvent.TheEvent += async (s, e) =>
{
try
{
await SomeTaskYouWantToAwait();
}
catch (Exception ex)
{
}
}
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:
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:
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:
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 EventHandler
s 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 EventHandler
s 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 EventHandler
s 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).