Back to Blog
C#

C# Event Null Check: Safe Invocation Patterns

c# event null check: Learn how to safely check and invoke C# events to avoid NullReferenceException, including thread-safe patterns and the null-conditional operator.

C# eventsNullReferenceExceptionThread safetyDelegate invocation.NET
Illustration of a C# event being safely invoked with a null-conditional operator, showing a delegate reference being checked before invocation.

In C#, an event with no subscribers is null. Calling it directly throws a NullReferenceException. The standard way to perform a c# event null check is to use the null-conditional operator: Event?.Invoke(...). But the story doesn't end there. The way you check and invoke events affects thread safety, performance, and how your code behaves under concurrency.

What Happens When an Event Has No Subscribers

A field-like event is backed by a delegate field. When no subscriber has been added, that field is null. Attempting to invoke it directly fails:

public event EventHandler? SomethingHappened; public void Raise() { SomethingHappened(this, EventArgs.Empty); // NullReferenceException if no subscribers }

The exception occurs because the compiler translates the event invocation into a call on the delegate's Invoke method. A null delegate has no Invoke method to call.

Before C# 6, the typical guard was an explicit null check:

public void Raise() { var handler = SomethingHappened; if (handler != null) { handler(this, EventArgs.Empty); } }

This works, but it requires a local variable to avoid a race condition, which we'll examine shortly.

The Null-Conditional Operator for Event Invocation

C# 6 introduced the null-conditional operator ?., which provides a concise and safe way to invoke events:

public void Raise() { SomethingHappened?.Invoke(this, EventArgs.Empty); }

The operator checks whether the left operand is null. If it is null, the entire expression evaluates to null and Invoke is not called. If it is not null, Invoke runs. Crucially, the left operand is evaluated only once, which means the delegate reference is captured atomically. This makes ?.Invoke equivalent to the explicit local-variable pattern above, but with less code.

This is the recommended way to perform a c# event null check in modern code. It is clear, concise, and avoids the NullReferenceException without cluttering the method.

Thread Safety and the Race Condition

The reason the local-variable pattern exists is a classic race condition. Consider this naive check-then-invoke code:

public void Raise() { if (SomethingHappened != null) { SomethingHappened(this, EventArgs.Empty); } }

Between the null check and the invocation, another thread could remove the last subscriber, setting the event to null. The invocation then throws a NullReferenceException. The local-variable pattern prevents this by reading the delegate into a local reference once:

var handler = SomethingHappened; if (handler != null) { handler(this, EventArgs.Empty); }

Now the invocation uses the local handler, which is guaranteed non-null at the point of the call. The null-conditional operator ?. does the same thing internally: it reads the delegate reference once, checks it, and invokes on that same reference. Therefore, ?.Invoke is thread-safe in the sense that it will not throw a NullReferenceException due to a race.

However, thread safety does not mean the invocation is atomic with respect to subscriber changes. If another thread removes a subscriber after the delegate reference is captured, that subscriber will still be invoked. That is usually acceptable because the event is a snapshot of the subscription list at the moment of invocation. If you need stricter ordering guarantees, you need a different synchronization mechanism, not just a null check.

When Custom Add/Remove Accessors Change the Picture

Field-like events are the most common, but you can define custom add and remove accessors. When you do, the event is no longer backed by a simple delegate field; it might be backed by a different storage mechanism, such as a list of delegates or a thread-safe collection. In that case, the null-conditional operator still works because it operates on the event's underlying delegate, which is the result of the accessor's getter. But the behavior depends on how you implement the accessors.

private EventHandler? _handlers; public event EventHandler? SomethingHappened { add { _handlers += value; } remove { _handlers -= value; } }

Here, the event's delegate is _handlers. The ?.Invoke pattern works the same way. If you use a List<EventHandler> or a concurrent collection, you must write your own invocation logic that iterates the collection safely. The null-conditional operator is not directly applicable because the event itself is not a delegate. In such custom scenarios, you need to design the invocation method carefully, often by copying the collection to a local array before iterating.

Performance and Allocation Considerations

The null-conditional operator does not introduce measurable overhead in most applications. It compiles to a single ldfld instruction to read the delegate, a brtrue to check for null, and then a callvirt to invoke. The local-variable pattern is essentially identical. The main performance consideration is not the null check itself but the cost of invoking each subscriber. If you have many subscribers, the invocation is O(n) in the number of subscribers, and each subscriber call may block or do work. The null check is negligible.

One subtle point: using ?.Invoke creates a temporary copy of the delegate reference on the stack. This is a value copy of a reference, not an allocation. It does not cause garbage collection pressure. So there is no reason to avoid ?.Invoke for performance reasons.

If you are in a high-frequency code path and the event is rarely subscribed, the null check is a single branch that is well predicted. The cost is minimal. If you need to optimize further, you could use a sentinel empty delegate to avoid the null check entirely, but that adds complexity and is rarely worth it unless profiling shows a real bottleneck.

Choosing the Right Invocation Pattern

For most code, ?.Invoke is the right choice. It is concise, safe, and idiomatic. Use it unless you have a specific reason to do otherwise.

If you need to invoke the event on a specific thread (for example, on the UI thread), you might wrap the invocation in a Dispatcher or SynchronizationContext. The null check remains the same; you just change how the delegate is called. For example:

public void RaiseOnUiThread() { SomethingHappened?.Invoke(this, EventArgs.Empty); // Still safe }

If you are using a custom event accessor with a collection of handlers, you need a different pattern. You might copy the handlers to an array and then invoke each one, handling exceptions per subscriber if needed. The null-conditional operator is not sufficient because the event is not a single delegate.

Finally, consider the case where you want to pass the event's delegate to another method, such as Task.Run. You still need to capture the delegate safely. The ?.Invoke pattern is not directly usable because you need the delegate itself, not just the invocation. In that case, use the local-variable pattern:

var handler = SomethingHappened; if (handler != null) { Task.Run(() => handler(this, EventArgs.Empty)); }

This captures the delegate reference once and uses it later, avoiding the race condition and the null check at invocation time.

Understanding these patterns ensures that your c# event null check is not only syntactically correct but also robust in concurrent scenarios and appropriate for the event's storage model.

c# event null check: Practical Usage and Code Examples | RYUSLOG DEV