The Dangers of Handling Async Event Handlers in C#
March 13, 2024
• 1,331 views
There's no dancing around this one - async event handlers in C# are an absolute nightmare. We all know we're not supposed to use async void in our code. We're taught that from day 1 when using async await in C#. But when it comes to the signatures of events, we have no choice but to use async void for our async event handlers!
In this video, I'll demonstrate what's going on with async void so you understand exactly where the problems arise. I'll also share several improvements we can work with to try and make our lives easier.
View Transcript
async void is something that you're bound to come across if you're using async await and C at some point in your programming journey and you know by now that you're supposed to be avoiding async void of course but what happens when you're working with event handlers and you don't have any other choice because they need to have a void return type my name is Nick centino and I'm a principal software engineering manager at Microsoft in this video I'm going to explain to you what exactly happens when you're using async void and some patterns that we can use to try and make this a little bit better I'll give you a full disclaimer from the start there is no Silver Bullet for this solution except to not use them in the first place but we do have some techniques we can employ to make things better
overall a quick reminder to check that pin comment for my free Weekly Newsletter and my cours is on dome train let's jump over to visual studio all right we are going to start with an example so this code up at the top is what we're going to be starting with here and to explain what's going on I'm going to create an object that is the source of an event I'll show you that in just a moment we will hook up an event using this Anonymous delegate syntax to have this event handler here and then what we're going to do is start throwing exceptions inside of here and seeing what happens so let's go look at this event Source because it's very simple but you should see what it looks like so you understand what's going on truly all that it is is an event on
this object and then a method that we can call from the outside to force it to raise the event for us and the important thing that we need to note is the Syntax for an event handler if you look at what the intellisense is showing currently it says delegate void and then it has the type there object sender and then a type of event arcs because it is a generic but the void part is critical here because in order to hook up an event handler to an event you must use a void return type it's the only way that the syntax is going to match what's required for an event so this class is very simple just an event that we can raise from the outside let's scroll back up and see what else we have going on here so our event handler is going
to use the async void syntax I just have this commented out because technically you don't need it there it's implicit but I did want to show you that it is in fact implicit and is the same thing but we have the rest of the event handler syntax here and then inside of this event handler what we're going to do is have this try catch block and then we'll look at two different variations where we can await something that is awaitable and then call something that's not awaitable and this is realistically just the difference between having async void and an async task so let's go look at both awaitable and not awaitable methods here the not awaitable one is going to be an async void as I just mentioned and we can await things inside of it but we cannot await this method when we call
it back above what we're going to do in both of these methods here you can see we're going to throw an exception in both cases but in the awaitable one we're still going to have a task yield and this is because the method itself because it is a sync can await things itself but this one is in fact awaitable from the outside and the reason that it is is because it returns a task that's what's critical here to make Ace and Kuwait work and be able to handle exceptions properly so both of these throw an exception both of these are marked as async but only one has a task return type and the other is void we're going to start by running this program which you'll see on line 22 once we hook everything up it's just going to raise the event for us and
then we'll just wait for the console input to be able to terminate the program manually so all that we're going to do raise the event await this and see that we should hopefully be able to catch the exception because this is what we would expect in a normal asyn O8 program and you can see that we get gracefully caught the exception that's what we get from line 12 to 14 here when this is printed out so far not super exciting because this is what you would expect to see now the problem arises because we aren't able to await the not awaitable one as you can see when I hover over it it says cannot await void I already mentioned this I just wanted to sort of prove it to you which is why you end up writing code that looks just like like this and
unfortunately because we're not awaiting it inside here what this ends up doing is runs the code asynchronously and forgets about it that means that it continues running and when it finally goes to throw an exception this catch block is not going to be able to catch it for us and to prove it we'll go run it it did already break inside a visual studio so I'll continue to run and then when I go back here what you'll notice is that it's not getting our exception Handler where it says that it was gracefully caught instead what it says is unhandled exception it's funny that the order that this printed in because it says press enter to exit after unhandled exception but you can see that it ends up printing out this information here and it's not the same call stack that we would have expected if
we were catching it inside of our event handler so truly that TR catch block does absolutely nothing for us inside of our event handler when we're using async void I have one more extension of this before we start diving into some of the solutions that you can go look at but let's start by expanding these part two blocks so in part two of this what we're going to do is also put a TR catch around the outside the part that is in fact raising the event because the reality is that when it comes to hooking up events and event handlers you're not so concerned about this stuff inside of here and the reason that you shouldn't be is because the only spot you should have aying void is on the event handler itself everything else inside of here you know the drill you need to
be using a sync task so you should not ever have this stuff inside of your event handler you should only have this kind of thing and we saw that before this was working for us we saw that the try catch would in fact catch this but what I'm going to do now is I'm going to rethrow that exception so we're still going to have this get printed to the console then we're going to rethrow it and then we're going to see what happens when it again has this exception cross this async void boundary because this is truly where the problem happens once we hit an async void boundary in our call stack that's where we're not able to be able to catch exceptions properly so we saw that it worked before but we're going to rethrow it and that means if we have a TR
catch around this raise event should this be able to catch the exception for us because this is what we would want to work but remember raise EV ENT is going to go invoke the event and its void and you're going to have ACN code running behind it let's go see what happens when we run this code so again Visual Studio is breaking r away and if we go check to see what the console output is there's a lot of noise here so what's it say we do get the first one for gracefully caught the exception right so that comes from line 12 through 14 and that's inside of our event handler but then we throw that same exception so we've caught it we're going to rethrow it and we can see that once again we get an unhandled exception and that means just like we
would have expected that raising the event here and having the try catch around it is not going to catch exceptions that bubble out of event handlers this is truly what you need to be worried about when you're dealing with event handlers so now we've seen some errors being thrown inside of event handlers and how we can try to catch them unless they're ring void that's the whole problem and then we've seen exceptions that we could have caught inside the event handlers bubble back out all the way outside of the event handler to the spot where the event being raised but truly that's not where we can catch the exception and in fact there's no really great spot to catch these things this all comes back back to the fact that we cannot await on async void so what is the solution that I recommend for
this okay I'm going to give you a little bit of my background so you understand where I'm coming from when I tell you this information I've been building desktop application software for years prior to working at Microsoft so roughly the past four years I spent at least eight years in a digital forensic software company building Wind forms applications building WPF applications as my job prior to that I was building these things at startups in my internships over the course of 5 years prior to that I was building them on my own it's been well over a decade that I've been using events and event handlers and then at some point in there when we got a syn8 I started using it too this stuff is an absolute mess and I feel like there's not a perfect solution for this which is why I mentioned at
the beginning of this video but what I'm going to show you is already in the code and I just want to walk through why I think it's the best way it's the most straightforward and maybe some other things that you could build on top of this to help you out okay so the ultimate solution that I'm trying to recommend here is literally just making sure that you have a TR catch at the top level of your async Void method if you have to use an async void and you do in the case of event handlers because that's how the syntax works I urge you to use a TR catch at the very top level make it the very first thing that you do as part of this that way what you can do is ensure that there's no code here and there's no code here
that can throw exceptions the other thing is you don't want any code here that can throw exceptions either because like we saw in the examples already if we bubble exceptions anywhere out of this thing it can be disastrous for your program it could have totally unintended side effects so you need to be careful and I think personally the best way that you can guarantee this is with a try catch at the very top level that means all of the code that you write in here can absolutely throw exceptions doesn't matter because you're going to catch them and yeah these type of Pokemon got to catch them all exception handlers they suck but this is a perfect opportunity in my opinion to use them to not blow up the app the other reason I'm saying this is because there's going to be someone out there that
says well you shouldn't catch exceptions if you're not sure what's going on I get it if you want to have more specific exception handlers that's cool the problem is when these things throw exceptions and you're not catching them they're going to Bubble up somewhere else they might not crash your application entirely they will have totally bizarre side effects I do highly recommend you try to catch all of them here just so that you can do something with them that's going to lead us to the next Point what the heck is the something else that you do with them this is something that I cannot give you a solution for because it's going to be very tailored to your application whether or not you're using a service if you're building a UI application a game anything else right you're going to have different needs for the
application you're building some things that I would recommend are logging I think if you have logs and you're leveraging logs and whatever system you're building this would be a perfect opportunity to try and introduce some type of log that you can say oh crap we have something in our call stack on an event handler they could totally brick the application if we didn't have this so that's number one logging personally for everything I've ever had to build logging's been very important something else you could do depending on your application is telemetry if you have logging and depending on how your logging works and your access to them you might not worry about Telemetry but for me in a lot of the cases I've worked in even on products that weren't supposed to have internet connectivity but people would opt in for Telemetry having Telemetry to
be able to report exceptions like this is super powerful because now you get real time access to things like log information you can see when this type of stuff is causing problems so those are two ideas for things you can do but I think that you're going to want to consider this in your software design so talk with your team if you're working on a team and come up with patterns that you think will help you triage this kind of thing and understand what's going on so that you can go make sure that exceptions aren't actually being thrown I'm going to give you one more tip before I introduce something that I think could be really cool but I've not yet tried this and maybe one of you has so we'll come to that in just a moment but the next tip is going to
be about these things being fired off and forgotten about because I did mention that's what's happening so what we can also consider doing is using a cancellation token source so yes having the TR catch at the top level is going to be critical for us but if you're also worried about things maybe not throwing an exception but running off and taking forever or longer than you're hoping for again maybe without throwing an exception having these things be able to cancel themselves after an expiry period I think be very valuable so what we can do is make a new cancellation token Source I'm just calling it CTS yes these are disposable so put your using block on here as well and we can make this expire after 5 Seconds what we can do from there is think about the async O8 code that we would call
inside of here I did mention that you're not going to want to be using async void anywhere else inside of here right so I did mention that so what we should be able to do is look at our async awaitable code that we're calling and make sure that we can pass this cancellation token from this cancellation token Source in through those methods so that they can Implement cancellation what that would look like is going to modify things that don't have cancellation support just like this one here and we can put the cancellation token rate on this method task. yield doesn't take a cancellation token so not a great example but if we were doing other asynchronous operations we would try to leverage this cancellation token and that way if we wanted to call the awaitable here we would say awaitable ctst token. token p in
and that way it would be able to cancel after an expiry period from there you could also go make sure that you're catching explicitly things like operation canceled exceptions and doing what you need to do in terms of cancellation maybe when it's expiring you don't actually care not really sure maybe you want to treat that logging or Telemetry differ so that you know hey look this thing didn't throw an exception and break everything but it was running for longer than we thought and if we didn't have this in place maybe we wouldn't have noticed is that try catch and cancellation tokens to be able to expire long running async void methods I think these are super helpful but the third thing that I think would be super cool and if one of you has built this I would love to hear about it but I
think it would be awesome if someone could Implement a Roslin analyzer that could start enforcing some of these patterns for us the reason I say this is because I have tried to write code that would try to encapsulate some of what we see on the screen right now but it still requires that you go use it and that means that it's super easy someone coming into a code base and trying to hook up events just like they might expect they don't even know that they need to go use your class with the helper method on it that goes and does this stuff so I think that's a cool approach but I think having something like a Roslin analyzer or maybe some other type of static analysis if you're familiar with it would be really cool to implement and leverage certainly if I had a rule
like that that I could run from Visual Studio I would love to use it and that is going to wrap it up for async event handlers we got to see see exactly how and why async void is a total pain in the butt don't use it if you have to use it because of async event handlers like we walk through because there's no choice based on the signature then I highly recommend you have a try catch at the very top level and if you want to think about how you can try to make them expire in case they're off running out into outer space having something that can expire with a cancellation token would be an awesome bet as well and like I said if you know about rosin anal iers and you know how to go build this I think that would be cool
and I'd love to hear about it in the comments if you thought this was pretty cool and you want to hear more about acing void you can check out this video next thanks and I'll see you next time
Frequently Asked Questions
What is the main issue with using async void in C#?
The main issue with using async void is that it doesn't allow for proper exception handling. When an exception is thrown inside an async void method, it cannot be awaited, which means that any exceptions that occur will not be caught by try-catch blocks outside of that method. This can lead to unhandled exceptions that crash the application.
What should I do if I have to use async void for event handlers?
If you have to use async void for event handlers, I recommend placing a try-catch block at the very top level of your async void method. This way, you can catch any exceptions that occur within the method, preventing them from bubbling up and causing unintended side effects in your application.
Are there any strategies to manage long-running async void methods?
Yes, one strategy is to use a cancellation token source to allow the async void method to cancel itself after a specified period. This can help manage long-running tasks that may not throw exceptions but could still lead to performance issues. Additionally, you can implement logging or telemetry to monitor these tasks and handle any potential issues.
These FAQs were generated by AI from the video transcript.