async programming
event handlers
stateful event arguments
programming tutorials
software development

Async event handlers with stateful event args

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Async event handling is difficult when event arguments are mutable and shared between subscribers. One handler can mutate state while another is awaiting, creating nondeterministic behavior that is hard to debug. A safer architecture treats event payloads as immutable snapshots and defines explicit execution rules for handlers.

Why Mutable Event Args Break Predictability

In synchronous event models, handler order and state changes are easier to reason about. With async handlers, execution pauses at await, so shared event argument objects can change before continuation resumes.

csharp
1using System;
2using System.Threading.Tasks;
3
4public sealed class DataEventArgs : EventArgs
5{
6    public string Message { get; set; } = string.Empty;
7}
8
9public sealed class Publisher
10{
11    public event Func<object, DataEventArgs, Task>? DataReceived;
12
13    public async Task RaiseAsync(string text)
14    {
15        var args = new DataEventArgs { Message = text };
16        if (DataReceived is null) return;
17
18        foreach (var d in DataReceived.GetInvocationList())
19        {
20            var handler = (Func<object, DataEventArgs, Task>)d;
21            await handler(this, args);
22        }
23    }
24}

If one handler modifies args.Message, later handlers see altered state rather than original payload.

Prefer Immutable Event Payloads

A strong baseline is immutable event args. Immutable payloads remove accidental cross-handler coupling.

csharp
1using System;
2
3public sealed class ImmutableDataEventArgs : EventArgs
4{
5    public string Message { get; }
6    public DateTime OccurredAtUtc { get; }
7
8    public ImmutableDataEventArgs(string message, DateTime occurredAtUtc)
9    {
10        Message = message;
11        OccurredAtUtc = occurredAtUtc;
12    }
13}

With immutable payloads, each subscriber can trust that values remain stable throughout async execution.

Sequential Versus Parallel Handler Execution

Your event pipeline should explicitly choose one model:

  • sequential await for deterministic ordering and easier reasoning.
  • parallel Task.WhenAll for throughput when handlers are independent.

Parallel example:

csharp
1public event Func<object, ImmutableDataEventArgs, Task>? DataReceived;
2
3public async Task RaiseParallelAsync(string text)
4{
5    var args = new ImmutableDataEventArgs(text, DateTime.UtcNow);
6    if (DataReceived is null) return;
7
8    var tasks = DataReceived
9        .GetInvocationList()
10        .Cast<Func<object, ImmutableDataEventArgs, Task>>()
11        .Select(h => h(this, args));
12
13    await Task.WhenAll(tasks);
14}

If ordering matters, avoid parallel fan-out.

Snapshot Local State Before Await

Even with immutable args, handlers may depend on other mutable external state. Snapshot what you need before awaiting.

csharp
1public static async Task HandleAsync(object sender, ImmutableDataEventArgs args)
2{
3    string message = args.Message;
4    DateTime seenAt = DateTime.UtcNow;
5
6    await Task.Delay(50);
7    Console.WriteLine($"{seenAt:o} -> {message}");
8}

This keeps handler logic stable when external state mutates during async gaps.

Error and Cancellation Strategy

Async event systems need explicit failure behavior:

  • fail-fast and stop remaining handlers.
  • run all handlers and aggregate exceptions.
  • cancel on timeout using CancellationToken.

Without a documented policy, teams implement inconsistent behavior across publishers.

You should also avoid async void except where required by UI framework event signatures. Task-returning delegates make error propagation and testing significantly easier.

Contract Design for Stateful Scenarios

If mutable args are unavoidable, declare mutation ownership rules clearly. For example, only publisher may mutate, subscribers are read-only. Enforce this with interface design rather than conventions alone.

A practical alternative is to split mutable workflow data from event notification payload. Event consumers receive immutable facts, while mutating state lives in dedicated services with explicit APIs.

Common Pitfalls

  • Sharing one mutable event args object across async subscribers.
  • Mixing sequential and parallel handler execution without clear contract.
  • Using async void handlers and losing exception visibility.
  • Not defining timeout or cancellation behavior for slow subscribers.
  • Letting subscribers mutate shared workflow state implicitly.

Summary

  • Mutable event args plus async handlers can produce nondeterministic bugs.
  • Prefer immutable event payloads for stable subscriber behavior.
  • Decide explicitly between sequential and parallel handler execution.
  • Snapshot required values before awaiting in handlers.
  • Define exception, cancellation, and mutation contracts up front.

Course illustration
Course illustration

All Rights Reserved.