1. Introduction
Working with delegates and events in C# is pleasant and convenient — the language does a lot for you. But behind that curtain there are many pitfalls: invisible memory leaks, weird bugs from double subscription, handler desynchronization and even sudden exceptions during notification broadcasting. Delegates and events are powerful but require careful handling of object lifetimes, understanding of threading and knowledge of how invocation and unsubscription actually work. If your events work "almost always" but sometimes don't fire or cause strange errors — you're definitely not alone! Let's figure out where even experienced developers most often miss things and how to avoid it.
Table of common mistakes
| Mistake | Consequence | How to avoid |
|---|---|---|
| Double subscription | Handler is called multiple times | Keep track of subscriptions, remove before adding |
| Not unsubscribing (memory leak) | Subscribers stay in memory, “zombies” | Always unsubscribe, use IDisposable |
| Exception in a handler | Remaining handlers won't be called | try/catch in handlers or iterate manually |
| Modifying subscribers during event | Skipping or duplicate calls | Iterate a copy of handlers (GetInvocationList()) |
| Calling event when MyEvent == null | NullReferenceException | Check for null, use ?.Invoke |
| Handler signature mismatch | Compilation error | Check signatures |
| Invoking event from outside | Compilation error | Invoke only via OnEventName |
| Static events in the wrong place | Subscription mixing | Don't make static without a good reason |
| Closure issues with lambdas | Unexpected values | Make a copy of the variable |
2. Multiple subscription and multiple invocations
The essence of the mistake
If you subscribe the same handler to an event several times, each += adds your method to the delegate invocation list. As a result the handler will be called as many times as it was added.
How does this show up?
Imagine you have a button and a click handler:
Button btn = new Button();
btn.Click += OnButtonClick; // We subscribe
btn.Click += OnButtonClick; // And here's the duplicate subscription!
Now on each button click the OnButtonClick method will be called twice. If inside the handler you update a counter or append a log entry, you'll see doubled results.
How to fix it?
Usually duplicate subscription happens because of code structure issues — for example, when += is placed in a method that gets called multiple times (in different lifecycle phases).
- Keep track of where subscriptions happen.
- Don't put += into lifecycle steps that can run repeatedly.
- Sometimes it's useful to use a "unique" subscription — remove the handler before adding it:
myEvent -= MyHandler; // Remove just in case
myEvent += MyHandler; // Then subscribe again
This is safe: if the handler wasn't there, -= does nothing.
3. Zombie subscriber
The essence of the mistake
If a subscriber subscribes to a long-lived publisher and doesn't unsubscribe, the garbage collector can't collect it: the publisher still holds a reference to the handler delegate, and therefore to the whole subscriber object. Result — memory leaks.
Typical example
public class TemporaryPopup : IDisposable
{
private Window _hostWindow;
public TemporaryPopup(Window window)
{
_hostWindow = window;
_hostWindow.Closed += OnHostClosed;
}
private void OnHostClosed(object sender, EventArgs e)
{
// ...
}
public void Dispose()
{
_hostWindow.Closed -= OnHostClosed; // Don't forget to unsubscribe!
}
}
If you forget about Dispose() or don't call it, then even if you remove all references to TemporaryPopup, the object won't be collected — the window still references its handler.
How to avoid it?
- Implement IDisposable in subscribers if their lifetime is shorter than the publisher's.
- Use the using pattern or call Dispose() explicitly:
using (var popup = new TemporaryPopup(mainWindow))
{
// ...
} // Dispose is called here automatically
In GUI apps — unsubscribe when a window/form closes (for example, in close handlers or in the form's Dispose()).
4. Handling exceptions inside handlers
The essence of the mistake
When an event invokes dozens of handlers and one throws, the remaining handlers won't execute.
Demonstration
public event EventHandler MyEvent;
public void Raise()
{
MyEvent?.Invoke(this, EventArgs.Empty);
}
If one of the methods subscribed to MyEvent throws, the others won't be called — the invocation chain is broken.
How to deal with such errors?
- In event handlers either handle exceptions locally (try/catch) or knowingly let them bubble.
- In complex scenarios — iterate subscribers manually and isolate each error:
var handlers = MyEvent?.GetInvocationList();
foreach (var handler in handlers)
{
try
{
handler.DynamicInvoke(this, EventArgs.Empty);
}
catch (Exception ex)
{
// Logging, emergency recovery
}
}
5. Changing the subscriber list during event broadcasting
The essence of the mistake
If a handler unsubscribes itself or others inside the event, it can disturb the invocation order: some handlers will be skipped or called multiple times.
How to avoid it?
- Don't modify subscriptions from within handlers.
- If needed — iterate a copy of the delegate list:
var handlers = MyEvent?.GetInvocationList();
foreach (EventHandler handler in handlers)
{
handler(this, EventArgs.Empty);
}
6. Confusion with a null delegate (no subscribers)
The essence of the mistake
If nobody is subscribed, the event delegate equals null. Invoking it without a check leads to NullReferenceException.
Poor example
public event EventHandler MyEvent;
public void Raise()
{
MyEvent(this, EventArgs.Empty); // If there are no subscribers — exception!
}
How to do it correctly?
- Use safe invocation: MyEvent?.Invoke(this, EventArgs.Empty).
- Or use the classic thread-safe pattern: copy the delegate to a local variable and invoke it.
7. Mixing delegates/events of different signatures
The essence of the mistake
Delegates are strongly typed. A mismatch between a handler's signature and the event's delegate is a compile error.
Example
public event EventHandler<string> TextChanged;
void WrongHandler(object sender, int number) { /* ... */ }
TextChanged += WrongHandler; // Compile error!
Use standard delegates EventHandler and EventHandler<T>, and ensure exact signature match.
8. Trying to invoke an event outside the publisher class
The essence of the mistake
event is an encapsulated delegate: external code can't invoke it directly (only add/remove handlers).
Example
public class MyPublisher
{
public event EventHandler SomethingHappened;
}
var publisher = new MyPublisher();
publisher.SomethingHappened?.Invoke(publisher, EventArgs.Empty); // Error!
How to do it right?
Invoke via a protected/public method on the publisher — usually OnEventName. From outside — only +=/-=.
9. Mistakes with add and remove accessors
The essence of the mistake
Custom accessors for events let you control subscription, but it's easy to break correct invocation ordering or thread-safety.
Example
public event EventHandler MyEvent
{
add { /* ... */ }
remove { /* ... */ }
}
If you're not sure — use the default event implementation. If you implement custom accessors, consult docs and consider synchronization.
10. Accessing events from static and instance contexts
The essence of the mistake
It's easy to accidentally declare an event static where it should be instance-level. Then all objects share the same subscriber list.
Example
public static event EventHandler GlobalEvent; // Oops!
// Instances lose individuality, subscriptions merge into a common pile
How to avoid?
Make an event static only when you actually need a global-level event (for example, a global log). Otherwise keep events on the instance level.
11. Capturing variables in lambda expressions
The essence of the mistake
Lambdas capture variables by reference. In loops this often leads to the "last value" problem.
Example
for (int i = 0; i < 5; i++)
{
button.Click += (s, e) => Console.WriteLine(i);
}
// All handlers will print "5"
How to do it correctly?
for (int i = 0; i < 5; i++)
{
int copy = i; // Local copy
button.Click += (s, e) => Console.WriteLine(copy);
}
12. Mixing weak and strong references: Advanced “Weak Events”
In large apps (for example, WPF) a «weak events» mechanism is used where the publisher keeps weak references to subscribers so as not to prevent garbage collection. Weak events help prevent leaks, but a subscriber can be collected and miss the event.
Details: Weak Event Patterns (MSDN)
13. Lack of naming and signature standards for events
The essence of the mistake
Follow standard signatures and names: events — in past tense (Changed, Closed, Completed), arguments — inherit from EventArgs.
“Wrong” example:
public delegate void SomethingHappens(int what);
// ...
public event SomethingHappens Something;
“Right” example:
public event EventHandler<EventArgs> SomethingHappened;
For your own events almost always use EventHandler or EventHandler<T>. Your colleagues (and future you) will thank you.
GO TO FULL VERSION