Handling Multiple Event Exceptions in C

In C#, an event handler exception occurs when a subscriber's method throws an unhandled error during event invocation, which typically halts the invocation pipeline and prevents subsequent subscribers from executing. This article explains the mechanics behind event handler exceptions, the limitations of standard invocation patterns, and how to use the invocation list to ensure all subscribers run while aggregating any thrown exceptions for clean, predictable error handling.

Understanding Event Handler Exceptions

In C#, events rely on multicast delegates. When an event is raised using the standard invocation pattern—such as MyEvent?.Invoke(this, EventArgs.Empty)—the runtime executes each subscriber's method sequentially on the calling thread.

If any subscriber throws an unhandled exception, the execution chain breaks immediately. The calling code receives that single exception, but any subscribers remaining in the invocation queue are skipped entirely. This behavior can leave your application in an inconsistent state, as some parts of your system never receive critical state updates.

The Problem with Default Invocation

Consider an event with three subscribers: Subscriber A, Subscriber B, and Subscriber C.

public event EventHandler ProcessCompleted;

public void CompleteProcess()
{
    // If Subscriber A throws an exception, Subscribers B and C will never execute.
    ProcessCompleted?.Invoke(this, EventArgs.Empty);
}

If Subscriber A throws an exception, the invocation loop terminates. Subscriber B and Subscriber C never execute, and you lose visibility into whether B or C would have succeeded or thrown additional errors.

Handling Multiple Throwing Subscribers

To ensure that every subscriber executes regardless of failures elsewhere in the chain, you must manually iterate through the delegate's invocation list and isolate each call in a try-catch block. You can collect any errors that occur and wrap them in an AggregateException.

Implementation Pattern

Use the Delegate.GetInvocationList() method to obtain an array of individual delegates attached to the event.

using System;
using System.Collections.Generic;

public class EventPublisher
{
    public event EventHandler<EventArgs> DataProcessed;

    public void OnDataProcessed()
    {
        var handler = DataProcessed;
        if (handler == null)
        {
            return;
        }

        var exceptions = new List<Exception>();

        // Get each individual subscriber delegate
        foreach (Delegate singleCast in handler.GetInvocationList())
        {
            try
            {
                // Explicitly invoke each subscriber individually
                singleCast.DynamicInvoke(this, EventArgs.Empty);
            }
            catch (Exception ex)
            {
                // DynamicInvoke wraps exceptions in a TargetInvocationException;
                // capture InnerException to preserve the original error.
                exceptions.Add(ex.InnerException ?? ex);
            }
        }

        // If any subscribers threw exceptions, throw an AggregateException
        if (exceptions.Count > 0)
        {
            throw new AggregateException("One or more event subscribers encountered an error.", exceptions);
        }
    }
}

Strongly Typed Invocation Alternative

Using DynamicInvoke introduces slight reflection overhead. If performance is critical, cast each item in the invocation list to the specific delegate type:

foreach (EventHandler<EventArgs> singleCast in handler.GetInvocationList())
{
    try
    {
        singleCast(this, EventArgs.Empty);
    }
    catch (Exception ex)
    {
        exceptions.Add(ex);
    }
}

Directly invoking the cast delegate removes the need to unwrap a TargetInvocationException, as the original exception is caught directly.

Handling Asynchronous Subscribers

If your event uses an asynchronous pattern (such as returning Task), iterate through the subscribers, invoke them, collect their returned Task objects, and await them collectively using Task.WhenAll. This ensures all asynchronous tasks run to completion while preserving all thrown exceptions inside the resulting AggregateException.