paint-brush
What You Need to Know About Async Event Handlers in C#by@devleader
882 reads
882 reads

What You Need to Know About Async Event Handlers in C#

by Dev LeaderMarch 18th, 2024
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Async event handlers in C# introduce complexities and risks due to the async void declaration, such as unobserved exceptions and concurrency concerns. Safely manage them by wrapping with try/catch blocks, enforcing timeouts, and adopting best practices for exception handling and cancellation.
featured image - What You Need to Know About Async Event Handlers in C#
Dev Leader HackerNoon profile picture

Events and event handlers aren’t necessarily the most common language feature being used if you’re focused on web development in ASP.NET Core… but if you’re building applications in WinForms, WPF, Maui — or anything with a user interface really — it’s almost guaranteed you’ll be using these. Given how pervasive async await code is now, that also suggests you’re probably going to run into async event handlers in C# at some point too.


Async event handlers have their challenges. They’re essentially two things that conceptually fit nicely together, but the language features of C# break down for us right where these two things intersect. In this article, I’ll explain why there are risks, what those risks are, and what you can try and do to make things better.


Understanding Async Event Handlers in C#

Async event handlers in C# allow you to handle asynchronous events that occur during the execution of a program. These events can include user input, network requests, or any other asynchronous operations. These methods are marked with the async keyword, indicating that they contain asynchronous operations, and therefore we can await other calls within them. When an event is triggered, the async event handler is invoked and runs concurrently with the rest of the program.


A traditional synchronous event handler in C# has the following syntax:

void MyEventHandler(object sender, EventArgs e)
{
    // TODO: handle the event
}


But if we need one to run asynchronously, it will look like this:

async void MyEventHandler(object sender, EventArgs e)
{
    // TODO: handle the event, running the asynchronous code we can await on
}


Keeping in mind that event handlers in C# *must* have a void return type, what kind of problem do we have here?

What Makes Async Event Handlers in C# Dangerous?

While async event handlers offer great flexibility and improved responsiveness, they also introduce a set of potential dangers and risks that you need to be aware of. And I don’t blame you if either you weren’t aware of these or you were aware but weren’t sure the best way to navigate these issues… because it’s a very awkward intersection of event handler syntax and async-await code.


Fundamentally, issues arise because of the async void declaration of the event handler. When we start using async await, we’re told early on that using async void is a big no-no.


And why is that? It eliminates the possibility for us to await the asynchronous operation — that’s what Task allows us to do. When we’re unable to leverage a task to await, we lose the ability to manage the execution of the task, including exception handling.


That means if your event handler goes boom, then your app is going to go boom somewhere else. And you won’t be able to gracefully deal with it.

Key Risks With Async Event Handlers in C#

Now that you understand what makes them dangerous in general, here are a few key risks associated with async event handlers in C#:


  1. Unobserved exceptions: When an async event handler encounters an exception during its execution, it can cause an unhandled exception if the error is not properly handled. This can lead to unexpected program behavior or even crashes. It’s important to handle exceptions appropriately to prevent these issues. This was the focus of the previous section.


  2. Concurrency concerns: Async event handlers run concurrently with the rest of the program, making it important to consider potential concurrency issues. Concurrent access to shared resources can lead to race conditions, data corruption, or inconsistent state. When we’re doing async await, sure, we are dealing with concurrency… But now we just have an async body of code running off potentially into outer space that we can’t align with.


And of course, the async flavor of event handlers in C# carries the same potential memory-leak issue. We’ll skip the details here but just remember to unhook events when you’re done requiring the registration and properly manage your event and event handler lifetimes!


Handling Async Event Handlers Safely in C#

Async event handlers in C# can introduce some potential issues if not handled properly. I’ve tried coming up with alternative solutions in the past to async void code as well as helpers that could clean up the syntax when trying to write safe event handlers. Ultimately, these are either incredibly complex or in the simple case the syntax just feels a bit off. This section will detail some general simple strategies you can employ to make these less of a headache — but the responsibility is still on you to add them!

Wrap Async Event Handlers with Try/Catch

If we accept that we’re stuck using [async void](https://www.devleader.ca/2023/01/27/async-void-how-to-tame-the-asynchronous-nightmare/) for event handlers, the biggest concern for us to address is catching exceptions. Once an exception bubbles up and hits that async void boundary, it’s game over. There’s not going to be an elegant way for your application to handle that. The current call stack will be unable to properly catch this even if you wrapped the event invocation in a try/catch.


To mitigate this risk, use a try-catch block at the top level of your event handler. Make it the first and last thing that your event handler does so there’s no way for code to throw exceptions without being wrapped with a catch block:

async Task MyEventHandler(object sender, EventArgs e)
{
    try
    {
        // TODO: do the async code
    }
    catch (Exception)
    {
        // Handle cancellation
    }
}


This still sucks because it’s manual, but maybe someone could make a Roslyn analyzer to enforce this?! Also, notice that I’m catching *all* exceptions here… But what are we expecting to do about it?


Truly, unless we’re ready for our application to crash or experience weird behavior, we need to stop exceptions from bubbling out of an async void method. Maybe logging or telemetry? But not a lot of great options by default.

Force Timeout on Async Event Handlers

Here’s a not-so-silver-bullet option you could add to the mix for working with async event handlers. Sometimes, async operations within an event handler can take longer to complete than expected. Recall that we can’t easily manage this async void call because we can’t await it and we don’t have a task object to work with. But if we could do *something* to help keep these from running away with resources in some sort of infinite loop, we could introduce a cancellation token timed with some maximum allowable time.


See the example below which includes the suggestion from the previous section:

async void MyEventHandler(object sender, EventArgs e)
{
    using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)))
    {
        try
        {
            // Continue with async operation, pass in cancel token
        }
        catch (OperationCanceledException)
        {
            // Handle cancellation
        }
        catch (Exception ex)
        {
            // TODO: how should we handle these?
        }
    }
}


In the code above, we simply try to limit execution to no longer than 5 seconds. As long as the async code we implement within here takes advantage of the cancellation token, then they should be able to handle canceling properly. But that does mean that *someone* still needs to carry that responsibility.


Wrapping Up Async Event Handlers in C#

In summary, understanding and safely managing async event handlers in C# is important for C# developers. Async event handlers have their own set of dangers and complexities that need to be addressed to ensure the stability and efficiency of software applications.


These can be challenging to be aware of in the first place, but even once you’re aware… what’s the best way to solve them? Hopefully from this article you understand why these are challenging and some strategies to help.


If you found this useful and you’re looking for more learning opportunities, consider subscribing to my free weekly software engineering newsletter and check out my free videos on YouTube!


Also published here.