Thanks for the great conversation in the comments!
I've added a follow up article to target a much more "simple" approach for event handlers since I unfortunately set myself up with this article to be super focused on it with the examples. The goal was to explore some interesting async void workarounds, but nevertheless, I think this follow up will be helpful for folks: Async EventHandlers – A Simple Safety Net to the Rescue[^]
Try/catch calls inside the async void method will catch exceptions before they are lost to the void. It was never a good idea to let exceptions escape event handlers even without async calls, so it can solve this same problem with less complexity.
someEventRaisingObject.TheEvent += async (s, e) =>
{
try
{
Console.WriteLine("Entering the event handler...");
await TaskThatIsReallyImportantAsync();
Console.WriteLine("Exiting the event handler...");
}
catch(Exception ex)
{
// Flag error state, record details, recover, etc.
}
};
If it's annoying to explicitly include try/catch statements for each event handler, a generic method or class could be created that wraps the handler definition and provide pre-packaged basic exception handling.
This is typically solved by AOP (Aspect Oriented Programming).
Consider this kind of tool: they are fantastic to identify each of your event handler and add try/catch with a dozen line of code: PostSharp | C# design patterns without boilerplate[^]
I have fixed thousands of similar issue by creating an Aspect class and referencing it on the AssemblyInfo of my winform project:
publicsealedoverridevoid OnException(MethodExecutionArgs args)
{
//on exceltpion, log but don't through the error but returns gracefuly
mylogHelper.Log_Exception(LogLevel.ErrorLevel, args.Exception);
args.FlowBehavior = FlowBehavior.Return;
}
and a couple of additional lines to identify the methods that are event handlers:
publicoverridebool CompileTimeValidate(MethodBase target)
{
return target.GetParameters().Any((p) => TrackedParameter(p));
}
//Filter to identify methods that have an EventArgs parameter
[NonSerialized()]
privatestatic Type eaType = typeof(System.EventArgs);
privatestaticbool TrackedParameter(ParameterInfo pi)
{
return eaType.IsAssignableFrom(pi.ParameterType);
}
and... that's all: it will fix all your event handlers, including the ones of your team mate or the one that you will create in future!
Agreed, AOP can afford some interesting opportunities. Unfortunately, some of the solutions like the one suggested have a high price tag.
Based on some of the feedback in the comments I'm going to publish two more alternatives for specifically looking at EventHandlers. I realize that I used EventHandlers as the basis for this discussion and admittedly the focus I wanted to have was more around some of the clever things we can do specifically for async void. EventHandlers just seemed to be the easiest example of it
Given that everyone has been very focused on alternatives for EventHandlers (and I don't blame them, I now realize I set myself up for this! ), I think it would be good follow up material to have some light-weight alternatives instead of hijacking sync contexts etc...
Thanks for the interesting and well written article. It seems to me that a useful way to get around the problems associated with an async void handler method is to call another handler that returns a Task from within the body of the async void handler. Something along these lines.
The above is based on advice given in Stephen Clearly's excellent article Best Practices in Asynchronous Programming. I was wondering, is there anything wrong with this approach?
Stephen Cleary is a very smart guy, so I don't mean to sound like I am opposing anything he's saying. However, I believe that his example is trying to show probably the best-effort way to separate out the event handler (with async void) from the actual async Task function. Fundamentally you can't wire up the async Task function to the event handler directly.
To answer the last part of your question though, as far as I know, my first code snippet in the article should hopefully provide a situation for what's "wrong" with this. And it's not that it's "wrong" but it's that there's a gap.
If you throw an exception inside of the async Task in your example (or anything at/below the async void) the exception handling does not operate as you'd likely hope. So if you did that and wrapped RaiseEventAsync with a try/catch block, you could not catch the exception with the async void. If you use the method from the article, it would allow you to capture the exception.
Hopefully that helps! And to be clear, I agree Stephen's proposal is essentially the way we're told to approach it. This was just trying to see if we could go a step further to catch exceptions as we might expect