Repository navigation
5.5.1.3. Exception Handling
Activities support graph-level exception handling in addition to the runtime IActivityExceptionHandler observability hook. Graph handlers are built as exception-handler nodes and can provide an alternate path when an action, accepted-event action, time-event action, or flow guard/transformation throws.
Call AddExceptionHandler<TException> in the builder callback for the node that should be protected. The handler receives an IExceptionHandlerContext<TException> with the exception, Activity node context, cancellation token, and output-token API:
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Stateflows;
using Stateflows.Activities;
public sealed record ReservationOutcome(string Status);Inside the host's registration code, with application services and orderId in scope:
builder.Services.AddStateflows(stateflows => stateflows
.AddActivities(activities => activities
.AddActivity("OrderProcessing", activity => activity
.AddInitial(node => node.AddControlFlow("ReserveStock"))
.AddAction(
"ReserveStock",
async context =>
{
await inventory.ReserveAsync(orderId, context.CancellationToken);
context.Output(new ReservationOutcome("reserved"));
},
node => node
.AddFlow<ReservationOutcome>("Complete")
.AddExceptionHandler<InvalidOperationException>(async exceptionContext =>
{
logger.LogWarning(
exceptionContext.Exception,
"Could not reserve stock for order {OrderId}",
orderId);
exceptionContext.Output(new ReservationOutcome("reservation-failed"));
}))
.AddAction("Complete", async context =>
await orders.MarkProcessedAsync(orderId, context.CancellationToken),
node => node.AddControlFlow<FinalNode>())
.AddFinal())));In this example the action's output flow carries either the success or handled-failure outcome to Complete; the interrupted action does not resume. For broader coverage, register a handler on a containing Structured, Parallel, or Iterative Activity node; that can handle exceptions escaping descendant nodes in its nested scope.
The builder also supports a class-based handler:
using System;
using System.Threading;
using System.Threading.Tasks;
using Stateflows.Activities;
public sealed class InvalidOperationHandler : IExceptionHandlerNode<InvalidOperationException>
{
public Task HandleAsync(
InvalidOperationException exception,
CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}Register it on the protected node with .AddExceptionHandler<InvalidOperationException, InvalidOperationHandler>(). The handler type is created through Stateflows' model-element activator, so it can use constructor injection. The non-generic IExceptionHandlerNode is a convenience for handlers that work with any Exception.
When an exception occurs, Stateflows searches handlers attached to the node where it originated, then walks outward through containing structured Activity nodes. The nearest containing scope with a matching handler handles it. Within that scope, an exact exception-type match is preferred; otherwise a handler whose declared exception type is a base type of the thrown exception can match. For example, a handler for IOException can handle FileNotFoundException.
The exception is available as context.Exception. IExceptionHandlerContext<TException> also implements the Activity node context and output-token interfaces. A class handler receives the typed exception and cancellation token through HandleAsync.
Handlers added to a node are executable graph elements created by the fluent AddExceptionHandler API; users do not connect them with ordinary flows. An exception handler's output tokens are forwarded into the enclosing execution scope. Graph handlers cover failures during node execution and flow guard/transformation logic; they do not handle Activity or node initialization/finalization callbacks. Those lifecycle failures are reported through IActivityExceptionHandler and remain failed. When no graph handler matches, normal exception propagation continues, including runtime notifications through IActivityExceptionHandler where applicable.
During Orleans Activity streaming, graph handlers preserve the failing invocation's provenance and can emit recovery tokens through the usual graph machinery. Unhandled streamed failures are logged and publish immediate ActivityStreamFailure notifications with the Behavior ID, triggering event ID/name, and exception types. The failed invocation is isolated from unrelated admitted work, but already-applied live values and external effects are not rolled back.
These two APIs have different purposes:
| API | Role | Does it recover the Activity execution? |
|---|---|---|
AddExceptionHandler<TException>(...) / IExceptionHandlerNode<TException>
|
Model a local or enclosing-scope exception-handling path in the Activity graph | Yes, when a matching graph handler completes successfully |
IActivityExceptionHandler |
Observe/report failures from Activity initialization/finalization, node operations, and flow guards/transformations | No. It acknowledges/report exceptions but the failed execution remains failed |
See Observability for runtime exception hooks and their limits. Activity exception handlers are separate from State Machine exception handlers, which can acknowledge failures or allow them to be sent back to a State Machine as exception events.
Home page Support Code licensed under an MIT-style License. Documentation licensed under CC BY 4.0. © by Mikołaj Milewski, 2026