Using Async Delegates and Event Handlers in C

In modern C# development, asynchronous programming with delegates and event handlers allows applications to perform resource-intensive operations without blocking the main execution thread. While the standard async/await pattern is straightforward with methods, applying it to delegates and events requires understanding the difference between returning Task and returning void, managing exceptions, and avoiding common concurrency pitfalls.

Asynchronous Delegates: Use Func<Task> Instead of Action

When working with delegates that represent asynchronous operations, avoid using Action. An Action delegate returns void, which prevents the caller from awaiting the completion of the asynchronous task. Instead, use Func<Task> for operations without a return value, or Func<Task<T>> for operations that return a value.

// Defining an async delegate
Func<string, Task<int>> processDataAsync = async (data) =>
{
    await Task.Delay(500); // Simulate asynchronous I/O
    return data.Length;
};

// Invoking and awaiting the delegate
int result = await processDataAsync("Hello World");
Console.WriteLine($"Processed length: {result}");

Using Func<Task> guarantees that the calling code can properly await the delegate, observe any exceptions thrown, and control the flow of execution.

Standard Async Event Handlers

The standard .NET event pattern relies on delegates returning void (such as EventHandler or EventHandler<TEventArgs>). Because the signature requires a void return type, event handlers are the only acceptable use case for async void in C#.

public class Button
{
    public event EventHandler Clicked;

    public void OnClick()
    {
        Clicked?.Invoke(this, EventArgs.Empty);
    }
}

public class Consumer
{
    public void Subscribe(Button button)
    {
        // Subscribing with an async void handler
        button.Clicked += OnButtonClickedAsync;
    }

    private async void OnButtonClickedAsync(object sender, EventArgs e)
    {
        try
        {
            await Task.Delay(1000); // Async work
            Console.WriteLine("Button processing complete.");
        }
        catch (Exception ex)
        {
            // Always catch exceptions inside async void handlers
            Console.WriteLine($"Error occurred: {ex.Message}");
        }
    }
}

Exception Handling in async void Handlers

Because callers cannot await an async void method, any unhandled exception cannot be caught by the code that raised the event. Instead, an unhandled exception will be raised directly on the ambient SynchronizationContext, which typically crashes the process.

Always wrap the implementation inside an async void event handler in a try-catch block to handle errors safely.

Custom Asynchronous Events Using Func<Task>

When designing custom classes and you need the event publisher to await the completion of all subscribers, do not use the standard EventHandler. Instead, define an event that returns a Task.

public class AsyncDataProcessor
{
    // Define an async event delegate
    public event Func<object, EventArgs, Task> ProcessingStarted;

    public async Task ProcessAsync()
    {
        if (ProcessingStarted != null)
        {
            // Sequential invocation
            foreach (Func<object, EventArgs, Task> handler in ProcessingStarted.GetInvocationList())
            {
                await handler(this, EventArgs.Empty);
            }
            
            // Alternatively, for parallel invocation:
            // var tasks = ProcessingStarted.GetInvocationList()
            //     .Cast<Func<object, EventArgs, Task>>()
            //     .Select(h => h(this, EventArgs.Empty));
            // await Task.WhenAll(tasks);
        }

        Console.WriteLine("Core processing finished.");
    }
}

By retrieving the invocation list using GetInvocationList(), the publisher can await each subscriber individually or collect the returned tasks and execute them concurrently using Task.WhenAll. This ensures the publisher does not proceed until all subscriber tasks have finished successfully.