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.