BrandGhost

EASIEST solution for async void in C# EventHandlers

async and EventHandlers in C# just don't seem to mix very well. As soon as you have exceptions, you're in for a world of pain and often left wondering just what the heck broke your application. If you've been having headaches with async event handlers, then check this out! Want to read the article this was based on? https://www.devleader.ca/2023/02/14/async-eventhandlers-a-simple-safety-net-to-the-rescue/ Want more info about async event handlers? https://www.youtube.com/watch?v=aLN0J_RtDRg&li...
View Transcript
Asynchronous event handlers in C generally lead us into a pattern that has async void. When an exception is thrown, we can lose the context of that exception. In this video, we'll look at two simple solutions for how you can work around this problem. So, if you could do me a favor, please like this video and subscribe to the channel. And with that said, let's head right into the code. So, on my screen is a familiar example if you watch my other videos, but I'm just going to show the base case here for when we have an asynchronous event handler. An asynchronous event handler in this case is going to look like an async void method signature. And when we have something that we're awaiting inside of that that's going to throw an exception. What will end up happening is the following. If we look at the output of this program, we can see that the example starting will raise the event. We'll get into our handler and we'll get into the spot where the task throws an exception. You'll notice that it prints example complete and then afterwards we see unhandled exception. So even though we have a try catch around this code up here, this catch is never hit. And that's because of async void. I'm going to jump into the first solution that we have here. And it's actually really simple. It's just that I think it's something that you have to keep in mind every single time you want to go write an event handler. While it's a solution, I don't think it's the most obvious thing. But I still want to walk through what this looks like. When we're looking at async event handlers, this method signature is still an async void. The only difference that I've made here is that I've actually wrapped the task that we're awaiting in a try catch. If you do this, the exception will in fact get caught here. This is great because we're not going to have an unhandled exception that's potentially going to silently destroy our program. What's not so great about this solution though is that for every single event handler you want to go create, you basically have to remember to go put a try catch there. Let's go ahead and run this example. This is the output from our first solution that we're looking at. So when we read the console, we see starting task that throws a sync. The line after this is supposed to throw the exception. And what's important to note in this part compared to the previous example that I ran is that we do actually get our exception caught. And we can see that it's from the try catch that's inside of the event handler. You'll notice that after that's printed, we get event handler completed and then example complete. It's important to note here that there is no unhandled exception. So this particular solution did in fact solve the problem, but it does have the trade-off that you have to remember to go put a try catch in every single one of your asynchronous event handlers. So another variation that we can look at doesn't actually solve the problem of having to remember to do this for every async event handler that you have, but in my opinion makes a little bit more explicit when you go to wire up the handler to the event. For what we're going to look at, I'll show you what the example code looks like. And you'll notice here that I've now changed how this registration looks. I have something that says event handlers.try async and passes in the type of the event args. then I'm able to pass in an async task, not an async void. And then I'm also able to provide a call back when an exception is thrown. The variation that we have here, in my opinion, this reads a lot more clearly that we're trying to wire up a different type of event handler. So, let's go look at what this particular code does. So, here's the code for try async. And it shouldn't be too surprising because it's just using the previous solution that I discussed. All that I've done is packaged this up in a little static class with a static method. This isn't fundamentally any different than the previous example. It's just that I've wrapped it in a way that I think makes the code a lot more readable. So, what you'll be able to see here is that we still have the exact same try catch that we had before. All that I'm doing is creating a new event handler and wrapping what's being passed in. If we look at the type that's being passed in for callback, we'll note that it's a function that takes in two parameters. object and then the type of the event args as well as returning a task. So what this means is that the caller of try async will be passing in a task that we can go await. This means that we're not solving the async void problem by any means. We're simply just avoiding it entirely. So let's have another quick look at this code now that we've seen the implementation of tricync and we know that it's the same solution as what I just showed. When we look at try async and how we're calling this, we can see that we have an async and this is actually going to be a task not avoid. So we have an async task and then we're able to await another async task by chaining async tasks that we can await. We know that we'll be able to have proper exception propagation when something is thrown. And just a quick note that a slight variation that I have in one of the overrides for try async is actually allowing your exception handler callback to be an async task as well. Let's go ahead and run the code for this example. All right, so here's the output for the second solution that we're looking at. Unsurprisingly, we still don't see any unhandled exception and that's because it's simply using the previous solution which we saw working. You'll note that it says try async error call back here, which is just proof that this line, line 38, is actually being the one executed when an exception is caught. So between these two solutions, the fundamental thing that we're looking at here is just that you need to have some type of try catch inside of your asynchronous event handler. The second solution in my opinion just reads a little bit more clearly that you have the intention to do something a little bit different versus having potentially some other try catch code that maybe isn't as obvious when you're looking at the event handler itself. Another important thing to call out about this solution is that compared to some of the other content that I posted about handling async void, this particular solution allows you to work around async void problems and event handlers by completely avoiding using async void. It also means that if you're not the person that owns the class that will ultimately be raising the event, so say something like a Windows form control, a WPF control, or some other UI framework, if you consider something like a button and you're not the one who created that button, you don't have control over how that event is invoked. With these two solutions, you don't have to be the one that owns the class that raises the event. your event handler will still function properly and be able to catch exceptions despite having an async void ultimately wired up to the event handler. So those were the two quick solutions that we're going to look at. They don't involve anything special in your code, no packages, and really no special information about how some of this stuff operates under the hood. Simply a try catch and making sure that you're using it is going to solve your problem here. All that we needed to do was put a try catch in place. The second solution, in my opinion, just makes it a little bit more obvious what you're trying to accomplish when you're wiring up your event handler. So, with that said, I'd love to hear from you in the comments if you think this is useful or if you have a different pattern that you're planning on using for handling your asynchronous event handlers. So, thanks so much for watching. If you thought this video was good, please give it a thumbs up. And if you could subscribe to the channel, that would be awesome. I'd really appreciate it. And thanks. We'll see you next time.

Frequently Asked Questions

What is the problem with using async void in C# event handlers?

The main issue with using async void in C# event handlers is that if an exception is thrown within the async void method, it can lead to unhandled exceptions that may crash the application. This happens because the exception context is lost, and the try-catch blocks surrounding the async void won't catch those exceptions.

What are the two solutions presented for handling exceptions in async event handlers?

The first solution involves wrapping the awaited task in a try-catch block within the async void event handler. This way, any exceptions thrown can be caught. The second solution is to use a custom method, like event handlers.try async, which allows you to pass an async task instead of an async void, making the code clearer and avoiding the async void problem altogether.

Can I use these solutions if I don't own the class that raises the event?

Yes, you can still use these solutions even if you don't own the class that raises the event. The solutions allow your event handler to function properly and catch exceptions, despite the event being wired up to an async void method in a class you don't control.

These FAQs were generated by AI from the video transcript.
An error has occurred. This application may no longer respond until reloaded. Reload