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
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 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
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.
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.
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 by 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:
Cons:
With that said, this solution truly is simple, but I think we can do a little bit better.
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 to catch around the body of the event handler, but now we’ve bundled it up into an explicitly 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:
Cons:
While originally I set out to explore
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
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).