async void is a pattern many intermediate dotnet programmers are told to avoid like the plague when writing C# applications. However, they one time we're told it's okay is for asynchronous event handlers. In this video, we'll explore how you can use a curious solution for handling exceptions even when your method signature is async void!
Check out more on async event handlers:
https://www.youtube.com/watch?v=aLN0J_RtDRg&list=PLzATctVhnsgh7LXNuEP87DzVB-FvUvyNv
More resources for this video can...
View Transcript
If you have asynchronous code, you want to be async awaiting in an event handler. You're kind of left with no choice but to have async void in the signature of the method. If you've been doing this for a while, you might have realized or come across a situation where when an exception is thrown, everything seems to fall apart. So, this is why people say don't use async void. And I'm not here to tell you to use async void, but I'm here to kind of walk you through perhaps um a workaround strategy that can actually help you when you are using async void. So, if that sounds interesting, please give the video a thumbs up, leave a comment below, subscribe to the channel for more content like this, and let's dive into it. Awesome stuff. So, I published an article on my blog called Async Void:
How to Tame the Asynchronous Nightmare. was trying to come up with something nice and clever that might get people's attention. So, I'm gonna not read the blog to you, but I just wanted to kind of show you that it's here. Kind of goes through the process of some of the pitfalls of async void and I have some net fiddle links in here so we can actually try those out on this video together. So, the first net fiddle that we come across, sorry if that was a jarring experience going to a very white screen. I can't find a a dark theme for this site, but in this example, I wanted to be able to highlight some code that when you're calling async void, what actually happens when you start throwing exceptions. So, let's kind of go through what we have here. Um, it's a there's no
um no program.cs and static void main. So, just a top level kind of entry point here. We're going to print start to the console. We're going to try out these different scenarios and when we throw an exception, we're going to try to catch it and print that we caught it and then we will also print an end statement. So if we look at scenario one, it's basically saying that we can actually await an async task and when it throws an exception, we should be able to catch it, right? So if we look for async task, there's not much code in this file. And by the way, I'll link all of these in the video description so you can try them out as well. And if you check out the article itself, you'll be able to click through and see it. So when we run the
async task here, it's also going to print to the console and throw an exception. So let's give it a shot. Right? So we get the start entering async task about to throw and then it actually printed this message here. Right? So, we got into the exception handler, which is great because this is the behavior we would expect when we're using async await patterns. We want to be able to actually catch exceptions when they blow up on a a given task. So, that's normal expected behavior. But what happens when we start using async void? So, because we have an async void signature, I'm just going to scroll down to it, right? Async void. Because void is not a task, we're not actually able to await it. Right? So if I try to actually put the word await here, it doesn't work. So you cannot await an
async void. So when we go to run this, what happens? And just to double check here, async void is actually going to call, right? Async task, which is the other thing we just ran, and it's going to throw an exception. So let's run it. And you'll see that we get the start line. That's pretty standard. Right at the top, we see entering async void method. So that's right here. Pretty simple. We see entering async task method and about to throw. So if we go back to here, entering about to throw. And then we never see this getting caught here. So this exception handler doesn't actually catch the exception. And then we just see end. So what I tried to demonstrate in this example is that if you had some really critical code that was in this async void method and this async task, you know,
in your code path ended up blowing up, you would never actually see that you had an exception come up. That's pretty dangerous. Um, when we're programming things, it's uh, you know, you've probably heard fail early. You want to be able to when issues are happening, you want to know right away so that you don't continue execution of your program and have this weird behavior where you don't know what's going on. This is exactly the opposite of that scenario. So that's number two. Let's dive into number three here. So I'm saying, okay, we're going to purposefully wrap async void in a task. So here's a thought, right? We said, hey, we can't await it. So let's actually try putting it inside of a task. Now we can await it, right? So what happens when we run this? Well, similar behavior, but what's interesting is that you
see this unhandled exception line get printed here. So this is actually I'm assuming because net fiddle had a global exception handler set up. So this is slightly better, right? We have some context that some exception got thrown but it's not in our code the handling I mean so end is the last line of our code in this method that's running. So where does this come from? So like I said it likely a global exception handler that net fiddle has set up to wrap around the code that you're running. But the the point here is that this is something that could absolutely bring down your entire application. kind of scary. So when we look at scenario two and three, async void are really like here's two good examples where you want to avoid it because they're going to cause you a lot of pain and suffering
in your debugging of applications. Cool. So basically we're told don't use async void because of things like this. But oh by the way if you have to use event handlers that's the one exception. Right. So again, if we scroll through the article, kind of go through some of this content and then I'm going to show you. So this is just the same co code we were looking at. Then I'm going to show you this example over here where we can actually try u working around this. So again, links will be in the description. If you check out the article as well, it's linked in there, too. And just want to walk through what we have here for code. So we're going to have some object that we can raise events on. And I'm going to scroll to it really quick just so you can kind
of see what it looks like. You'll see that it has a public event on it. Super simple, just an event handler with event args. Um, this could be any event args that you want. I just picked the base ones for this example. Then we'll have a public method on it. This doesn't this pattern is like uh it's a little bit contrived. I just want to be able to control when we're firing the event for this example. So what I have here is the old way. So let me comment this back out. What you would normally do is something like this. So you're going to invoke the event pass in this and the event args. Right? So again, if you have custom event args, you pass those in and then we would just return a completed task because you can't actually uh await these things. Um,
and technically it would probably be a void and you wouldn't have a sync here, right? It would probably look more like this. And the problem is going to be that if you have an event handler, right, you can't actually hook this up. So, let's put that back. Okay. So, we're going to basically hook up an event. And you can see this is just an anonymous delegate here. So, the async part, this is basically going to translate to having an async void. And we're hooking up an event handler. We'll print that we're entering and exiting. We're going to await um await a task here. If we check out that task, right? Um we're going to try throwing exceptions. We're going to try writing to the console just so we can play around with it. We'll do the same thing in the last example. We'll try to
catch it and print that we caught it. And then we'll have some line at the end to check how things work. Okay, cool stuff. Let's go ahead and let's start by not throwing. Let's just print to the console. And by the way, I left these so that you can play around with them and actually try commenting out the exception throwing and like the happy path as well. So up to you to try that out. But this will not throw an exception now. And it's just going to use the standard kind of way of hooking up an event handler. And I don't know why, but um this net fiddle in particular, you can see it's spinning. It seems to take a little bit longer. But let's kind of go through what we're seeing here. Entering the event handler, right? So when we actually um call raise
event async here, we get this handler firing. Very nice. And then this line runs. And we know that because it says look at us writing to the console from a task. So that happens here, right? So we're awaiting a task.run. And then um this all returns bubbles back up. Didn't throw an exception. So we see press enter to exit. And what's interesting too is that if you look at the order that this printed at the console exiting the event handler actually came after press enter to exit, right? So truly it was running asynchronously. Kind of neat. Okay, so that's not too exciting because nothing exploded. Let's see what happens when we throw an exception. So we throw an exception and remember this is with async void. So this should look like the other thing we just saw in the previous net fiddle and we can
see it blowing up here. Right. So entering the event handler again. We're going to go run this task. And now that task throws an exception. And we did not get to see this. Right? So our exception handler did not just like the other example did not catch this exception that came from the task. So it says unhandled exception. Um and it says that this is the expected exception. So that's what we plan to throw, right? But it's not handled by our application. It's bubbled up. I'm like I said before, I'm pretty sure that net fiddle probably has a global exception handler wired up so that you can see stuff like this. But this is something that would in theory bring down your application. Okay, cool. So, how do we fix it? Well, we've been told just avoid it and unless it's an event handler, but
we have a case where it's an event handler and it's causing issues. So, there's actually some really cool code. Um, I did not come up with this myself. So, if I jump back to my blog here, let me scroll past. This is just all the code that you see in the net fiddle. So, there's the solution for it here. Um, one sec. A lot of stuff here, but this is actually from this other blog post from uh 2019 from Oleg Carasik. Apologies if I have your name incorrect here, but he actually has an outstanding article. Uh so check it out. If you hit hit up my post, you can go to his article and he details sort of like his experiments of going through and trying to come up with the solution for it. And it's absolutely awesome. Lot of great detail there. So highly
recommend it. But let's actually jump over to the code and we can see this um basically being corrected for us. Super awesome. So let's comment out the old way, right? and we're going to use the new way. So the new way here is this method. And if you go reading uh Oleg's blog, uh you'll see that my implementation slightly different. Um and I've not necessarily like reinvented it by any means. So I'm not trying to take any credit for that. I've just kind of changed things up a little bit so that I can um and it's not even shown here. The next fiddle has it. But I have more like extension methods. I use some aggregate exceptions and a couple other quality of life things that I like and that way I could integrate it into my codebase. But when we look at this method
signature, we're going to be passing in the delegate which is going to be the event, right? If you see here that gets passed in as the delegate and it is of type multiccast delegate. So this is pretty useful for us because we're able to kind of get some information about the delegate that's being passed in. A couple other parameters here. So we have force ordering and stop on first error. So these aren't super important, but there's some I guess uh configuration if you will about how your event invocation will happen. So if you're familiar with events, you know that you can subscribe multiple event handlers to a given event. Force ordering when that's on will execute the handlers as you might expect in the order that they're registered. If you turn this off, we can actually do um sort of like a a not a
fire and forget, but almost like a shotgun approach where we start running the different event handlers and they can happen out of order. Um I can't really think of a great scenario for that personally, but I figured it was easy enough to have included there, so let's do it. And then this other parameter for stop on first error. So if you wanted to actually have all of your event handlers run regardless of whether or not one throws an exception, you could leave this as false. Otherwise, you could leave it as true and just kind of blow up on the first error. So again, Oleg details a lot of this on his blog, but let's go through some of the interesting parts of this. So really what it comes down to is having a custom synchronization context. If that doesn't make a ton of sense to
you, again, read through Oleg's blog. It's really uh useful information about why and how that works. But what we're able to do is get the invocation list from the delegate. So these are all of the event handlers that are subscribed to it. And then we can actually check using reflection when we put that async keyword in front. Async state machine attribute actually goes onto that method. It's just not something that you type there as a as a developer. It's a kind of a syntactic sugar if you will. Then we end up having these completed and failed um callbacks. And really what we're looking at here is these get passed into the synchronization context. And how we end up using them is tracking whether or not we have an exception or if the event handler itself actually completed successfully. So you can see if we have
a sync here, right? So if we figured out based on the attribute that it's asynchronous, we're going to use our own custom synchronization context. We'll see that a little bit lower. What we're able to do that's really cool is that we call this dynamic invoke on the delegate. Args is just a parameter list that we can pass in. Then here's where it gets a little bit dangerous based on how you're calling this. If you don't provide the right set of parameters or the wrong types, you can have this explode. So, one of the dangers with this setup is that it's not compile time safe. In my example, where I'll show you after, I actually have this set up with extension methods that helps prevent this next part. Um, we're able to catch exceptions here. And the rest of this is really just kind of the
logic that we'll go through. and wait for things to finish up. Okay, so the last part that we're going to look at super quickly here is just the synchronization context. Again, this is right from Oleg's code. Um, he explains why he has this set up the way that he does and we're able to track the exceptions. So, let's run it. Let's see what happens. Let me scroll all the way back up to the top here. You can see that I have this invoke async here and I have the comment. It's the new way, right? and we'll try throwing that exception. So, exact same scenario as before. And hopefully we don't see unhandled exception this time. It's going to take its time apparently. Cool. So, entering the event handler, right? Let me scroll up a little bit. We see that then it says caught an exception
in our handler. So we never see this one here, right? And that's because this task that we await throws an exception. Then we're able to catch it here. So this is new otherwise it was unhandled. So we're able to catch it and then we can press enter to to continue. So this ended up going. So we actually got away with an async void and caught the exception. So that's proof that his method actually works to be able to get that information about the exception back onto the the call stack for us to use. Cool stuff. So that's the the crux of it. I'm just going to jump over to um so again in the blog I kind of talk about some of those details I just covered and then I have a section like what what did I contribute right? So I mentioned that this
is Oleg's implementation. I'm just going to talk about a couple of quality of life things that I did. So, let me jump over to this code here. Again, another net fiddle. And I just set up a couple examples here. You'll notice that this one is way shorter in terms of how much code there is. And that's because I have put a lot of his logic wrapped up in extension methods into a Nougat package. I'm not trying to get people to go use this. This is what I use my own code personally. It's on GitHub if you want to see it. If you want to use the package, that's cool, too. Uh but no pressure and we set it up the same the same way, right? So we have a an object where we can subscribe to things. So let me scroll down a little bit
again. This looks similar to before public event handler some method where we can call invoke and then I'm just demonstrating that I have this syntax here that's an extension method. So we can say invoke a sync rate on the event handler. pass in the sender, pass in the event args, whether or not we want to run them in order, and if we stop on the first error. So, I guess there's a little bit of a spoiler alert in the bottom of this because it looks like net fiddle ran this as soon as I loaded it. Um, so let's just read the output for what it did. I'll run it again just to kind of prove that it's not me um, you know, tricking you with some output here. So, I I did just press run. I just pressed it again. And what we see is
that we start right and that's right at the top here. I wired up these two event handlers and they are async void. I was able to simulate, you know, when some other task running in a background here that were printing from uh the first one and we're printing from the second one. And we see those and they're in order because I subscribe to the event in order here with this one and then the second one here. So that's expected. But what's really cool is that both of these threw an exception, right? Expected one and expected two. So I was able to put in some aggregate exception handling here. And it says one or more errors occurred. And we actually see both, which is awesome. So we can try tweaking this parameter to stop on first error just to see what it does. Right? So if
I press run here that actually looks like it's a a bug in my uh in my implementation. So I'm going to have to fix that. But you can see it does printing on one, printing on two. This changed. And what's actually trying to happen here is that on the first task, it's basically trying to set on the um the information for that task uh completion source. It's saying, "Hey, look, we have an exception. Let's assign it here." But it looks like um the spot that I'm assigning that's out of order. So, I'm going to patch that up. So, small detail. I apologize for that. Um but you can see that it didn't actually try doing the second one, right? So, if I put this back to false, this is supposed to let it catch both. And if it's true, it's supposed to stop on the
first one. Oh, and it looks like it's a race condition, right? So, I didn't realize that it says we caught the exception. So, it does actually catch it, but um it looks like there's still some async stuff going on here and throwing an unhandled exception. So the cool thing is that I can reproduce this. So I will do so. I'm not going to fool around with this ordered one. Um these examples aren't set up very well for that. But if you had perhaps some examples of longer running event handlers, you could try putting this to to true and false. You could switch it up and see. U because right now you're seeing that it's always doing one than two. But if these were longer running, you could actually show them being interled. And um it's kind of interesting to see how that works. So there
we go. That's primarily it. I just wanted to be able to demonstrate that there are some workarounds for async void. Um I still don't recommend that you, you know, try littering your code with it. However, if you find yourself writing stuff like this where you need to have an async void event handler because you need to await some asynchronous code, I do think that this is a viable method. Um again, shout out to Oleg for this implementation. I think it's really awesome. I use it a lot in some of my code because I have a lot of event handlers and it's been uh truly uh gamechanging for me because I was having stuff blow up behind the scenes and pulling my hair out. So hopefully you enjoyed this. If that was interesting, please give the video a thumbs up. Leave a comment below for your
thoughts on how you might use this and uh subscribe to the channel if you like more content like this. Thanks and we'll see you next