Event Best Practices

Vaibhav • September 11, 2025

In the previous article, we explored how to create and use custom event arguments to pass structured data with your events. You learned how to design EventArgs classes, raise events safely, and handle them using both named methods and lambdas. Now that you understand how events work and how to enrich them with meaningful context, it’s time to focus on best practices - the patterns and principles that help you write clean, scalable, and maintainable event-driven code.

Events are a powerful tool, but like any tool, they can be misused. Poor event design can lead to memory leaks, tight coupling, unpredictable behavior, and hard-to-debug systems. In this article, we’ll walk through the most important practices for declaring, raising, subscribing to, and managing events in C#. We’ll also explore naming conventions, thread safety, and how to avoid common pitfalls - all while reinforcing the patterns you’ve already learned.

Use the Standard EventHandler Pattern

Whenever possible, use the built-in EventHandler and EventHandler<TEventArgs> delegates. These follow a consistent two-parameter signature: object sender and EventArgs e. This makes your events predictable and compatible with tooling, frameworks, and other developers’ expectations.

public event EventHandler<ProgressEventArgs> ProgressChanged;

Avoid creating custom delegate types unless you have a compelling reason. The standard pattern is flexible enough for most scenarios and improves interoperability across your codebase.

Stick to EventHandler<T> for all events that need to pass data. It’s clear, consistent, and widely supported.

Design Focused EventArgs Classes

Your EventArgs classes should be small, focused, and immutable. Include only the data that’s relevant to the event. Use read-only properties and initialize them via constructor parameters. This ensures that event data is stable and predictable once the event is raised.

public class FileUploadedEventArgs : EventArgs
{
    public string FileName { get; }
    public long SizeInBytes { get; }

    public FileUploadedEventArgs(string fileName, long size)
    {
        FileName = fileName;
        SizeInBytes = size;
    }
}

Avoid exposing mutable fields or properties. Subscribers should be able to trust that the event data won’t change unexpectedly.

Use Clear and Consistent Naming

Event names should describe what happened - not what will happen. Use past-tense verbs like Completed, Changed, Triggered, or Raised. This makes it clear that the event is a notification, not a command.

For example:

public event EventHandler<DownloadEventArgs> DownloadCompleted;
public event EventHandler<StatusChangedEventArgs> StatusChanged;

Avoid vague names like OnSomething or DoSomething. Be specific and descriptive.

Prefix your event arguments classes with the event name (e.g., DownloadCompletedEventArgs) or suffix them with EventArgs to follow convention.

Raise Events Safely

Always use the null-conditional operator ?.Invoke() when raising events. This ensures that the event is only called if there are subscribers - preventing NullReferenceException.

DownloadCompleted?.Invoke(this, new DownloadEventArgs(...));

For performance-critical or multi-threaded scenarios, copy the delegate to a local variable before invoking:

var handler = DownloadCompleted;
if (handler != null)
{
    handler(this, new DownloadEventArgs(...));
}

This prevents race conditions where the event is unsubscribed between the null check and the invocation.

Unsubscribe When No Longer Needed

Always unsubscribe from events when the subscriber is no longer needed. This prevents memory leaks - especially in long-lived applications or when working with UI components.

downloader.DownloadCompleted -= OnDownloadCompleted;

If you use anonymous methods or lambdas, store them in a variable so you can unsubscribe later. Otherwise, you won’t be able to remove the handler.

Note: Events hold strong references to their subscribers. If you forget to unsubscribe, the subscriber may stay in memory even after it’s no longer needed.

Avoid Event Overuse

Events are great for notifications - but they’re not a replacement for method calls or direct communication. Don’t use events to control behavior or enforce logic. Use them to signal that something happened, and let subscribers decide how to respond.

For example, don’t use an event to ask a question or expect a return value. Events are one-way broadcasts - not two-way conversations.

If you need a response, use a callback delegate or return a value from a method.

Keep Event Handlers Focused

Event handlers should be short and focused. Avoid putting complex logic or long-running operations inside a handler. If needed, delegate the work to another method or queue it for background processing.

void OnDownloadCompleted(object sender, DownloadEventArgs e)
{
    LogDownload(e.FileName);
    NotifyUser(e.FileName);
}

This keeps your event handling clean and responsive - especially in UI or real-time systems.

Handle Exceptions Gracefully

If one event handler throws an exception, the remaining handlers won’t be called - unless you handle exceptions manually. For critical systems, consider wrapping each handler in a try-catch block:

foreach (Delegate d in DownloadCompleted?.GetInvocationList() ?? Array.Empty<Delegate>())
{
    try
    {
        d.DynamicInvoke(this, new DownloadEventArgs(...));
    }
    catch (Exception ex)
    {
        Console.WriteLine("Handler error: " + ex.Message);
    }
}

This ensures that all subscribers get a chance to respond - even if one fails.

Use Weak References for Long-Lived Publishers

If your event publisher lives longer than its subscribers (e.g., a static service or global manager), consider using weak references or a custom event manager to avoid memory leaks. This is an advanced topic, but worth exploring for large applications.

Alternatively, use WeakEventManager in WPF or similar patterns in other frameworks.

Document Event Contracts Clearly

Events are part of your public API - so document them clearly. Describe what the event means, when it’s raised, what data is passed, and what subscribers should expect. This helps other developers use your events correctly and avoid surprises.

/// <summary>
/// Raised when a file download completes.
/// </summary>
public event EventHandler<DownloadEventArgs> DownloadCompleted;

Include XML comments for both the event and the EventArgs class. This improves IntelliSense and makes your code self-documenting.

Summary

Events are a powerful feature in C# - enabling loose coupling, reactive design, and clean notifications. But with great power comes great responsibility. In this article, you learned the best practices for declaring, raising, and handling events. You now understand how to use the standard EventHandler pattern, design focused EventArgs classes, raise events safely, and manage subscribers responsibly.

By following these practices, you’ll write event-driven code that’s clean, scalable, and easy to maintain. You’ll avoid common pitfalls like memory leaks, tight coupling, and unpredictable behavior - and build systems that respond gracefully to change.

In the next article, we’ll explore Functional Programming Concepts - diving into higher-order functions, closures, and how C# supports functional-style programming through delegates and lambdas.