A Simple Safety Net for Async EventHandlers
Dealing with async EventHandlers in C# can be very problematic. Async void is a pattern cause headaches with exceptions. Check out this simple solution!
Join the DZone community and get the full member experience.
Join For FreeWhen 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 the 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 in 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:
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 the 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) =>
{
// 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 to 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. The 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 the 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).
Published at DZone with permission of Nick Cosentino. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments