Pipeline
Exception Handlers
Gracefully handling exceptions in the pipeline
Exception handlers allow you to intercept and handle exceptions thrown by handlers or other pipeline behaviors, without wrapping everything in try-catch blocks.
IMessageExceptionHandler
public interface IMessageExceptionHandler<in TMessage, TResponse, in TException>
where TMessage : IMessage
where TException : Exception
{
ValueTask<ExceptionHandlingResult<TResponse>> Handle(
TMessage message,
TException exception,
CancellationToken cancellationToken);
}ExceptionHandlingResult
The handler returns one of two results:
// Exception was handled — return a fallback response
ExceptionHandlingResult<TResponse>.HandledWith(fallbackResponse);
// Exception was NOT handled — let it propagate
ExceptionHandlingResult<TResponse>.NotHandled();Example: Fallback on Database Error
public class DatabaseExceptionHandler
: IMessageExceptionHandler<GetUserQuery, User?, DbException>
{
private readonly ILogger<DatabaseExceptionHandler> _logger;
private readonly ICacheProvider _cache;
public DatabaseExceptionHandler(
ILogger<DatabaseExceptionHandler> logger,
ICacheProvider cache)
{
_logger = logger;
_cache = cache;
}
public async ValueTask<ExceptionHandlingResult<User?>> Handle(
GetUserQuery message,
DbException exception,
CancellationToken ct)
{
_logger.LogWarning(exception,
"Database error for GetUserQuery({Id}), falling back to cache",
message.Id);
// Try to return cached data
var cacheResult = await _cache.GetAsync<User?>($"user:{message.Id}");
if (cacheResult.HasValue)
{
return ExceptionHandlingResult<User?>.HandledWith(cacheResult.Value);
}
// Can't handle — let exception propagate
return ExceptionHandlingResult<User?>.NotHandled();
}
}Example: Generic Error Logging
public class GenericExceptionHandler<TMessage, TResponse>
: IMessageExceptionHandler<TMessage, TResponse, Exception>
where TMessage : IMessage
{
private readonly ILogger<GenericExceptionHandler<TMessage, TResponse>> _logger;
public GenericExceptionHandler(
ILogger<GenericExceptionHandler<TMessage, TResponse>> logger)
{
_logger = logger;
}
public ValueTask<ExceptionHandlingResult<TResponse>> Handle(
TMessage message,
Exception exception,
CancellationToken ct)
{
_logger.LogError(exception,
"Unhandled exception in {MessageType}: {ErrorMessage}",
typeof(TMessage).Name, exception.Message);
// Let the exception propagate after logging
return new ValueTask<ExceptionHandlingResult<TResponse>>(
ExceptionHandlingResult<TResponse>.NotHandled());
}
}Registration
builder.Services.AddTurboMediator(m =>
{
m.WithExceptionHandler<DatabaseExceptionHandler>();
m.WithExceptionHandler<GenericExceptionHandler<IMessage, object>>();
});When to Use Exception Handlers
- Fallback responses — Return cached or default data when primary source fails
- Error logging — Centralized error logging without try-catch in every handler
- Error translation — Convert infrastructure exceptions to domain-specific exceptions
- Graceful degradation — Return partial results when some operations fail