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.