Exception Policies

Configure per-exception handling with composable retry, redelivery, and terminal actions.

Not every exception deserves the same treatment. A database deadlock might resolve on immediate retry. A downstream service outage needs minutes to recover. A validation error will never succeed no matter how many times you retry. Exception policies let you define per-exception handling strategies - retry, redeliver, dead-letter, or discard - as composable escalation chains in a single AddResilience call.

C#
builder.Services
.AddMessageBus()
.AddResilience(policy =>
{
// Validation errors are permanent - route straight to the error endpoint
policy.On<ValidationException>().DeadLetter();
// Duplicate messages are safe to drop
policy.On<DuplicateMessageException>().Discard();
// Database deadlocks resolve quickly - retry then redeliver
policy.On<NpgsqlException>(ex => ex.IsTransient)
.Retry(5, TimeSpan.FromMilliseconds(200))
.ThenRedeliver();
// Everything else: retry 3 times, then redeliver on a schedule, then dead-letter
policy.Default()
.Retry()
.ThenRedeliver()
.ThenDeadLetter();
})
.AddEventHandler<OrderPlacedHandler>()
.AddRabbitMQ();

How exception handling works

When a handler throws an exception, Mocha evaluates exception policies to determine what happens next. The decision flows through two pipeline stages - retry in the consumer pipeline and redelivery in the receive pipeline - before reaching the fault middleware as a last resort.

flowchart TD
    A[Handler throws exception] --> B{Exception policy\nmatches?}
    B -->|Match found| D{Terminal action?}
    B -->|No match| M[Route to error endpoint]

    D -->|Discard| E[Swallow exception\nMessage disappears]
    D -->|DeadLetter| F[Skip retry and redelivery\nRoute to error endpoint]
    D -->|No terminal| G{Retry configured?}

    G -->|Yes| H[Retry with configured settings]
    G -->|Disabled| K{Redelivery configured?}

    H -->|Success| I[Done]
    H -->|All retries exhausted| K

    K -->|Yes| L[Schedule redelivery\nMessage re-enters pipeline later]
    K -->|Disabled| M

    L -->|Success on redelivery| I
    L -->|All redeliveries exhausted| M

Each exception policy rule targets a specific exception type and defines an escalation chain. The chain controls which stages the message passes through and with what settings.

Exception matching respects inheritance. A policy on NpgsqlException also matches any subclass. When multiple rules could match, the most specific type wins - the same precedence as C# catch blocks.

Configure exception policies

AddResilience is the single entry point for all exception handling configuration. There is no separate AddRetry or AddRedelivery call - retry and redelivery settings are configured per-exception within the policy.

:::note Replacement semantics Calling On<T>() for the same exception type replaces the previous rule for that type - last write wins. If you call On<HttpRequestException>() twice without a predicate, the second call overwrites the first. The same applies to Default(): calling it again replaces the previous default rule. For example, the parameterless AddResilience() registers Default().Retry().ThenRedeliver(). If you later call AddResilience(p => p.Default().Retry(5)), the new default replaces the one registered by the parameterless overload. :::

Parameterless defaults

The parameterless overload registers a catch-all Default() rule with both retry and redelivery enabled using built-in defaults:

C#
builder.Services
.AddMessageBus()
.AddResilience()
.AddEventHandler<OrderPlacedHandler>()
.AddRabbitMQ();

This is equivalent to:

C#
.AddResilience(policy =>
{
policy.Default().Retry().ThenRedeliver();
})

Default() and On<T>()

ExceptionPolicyOptions exposes two methods for creating rules:

  • Default() - shorthand for On<Exception>(). Configures the catch-all behavior for any exception that does not match a more specific rule.
  • On<TException>() - configures behavior for a specific exception type.
  • On<TException>(predicate) - configures behavior for a specific exception type when a predicate matches.
C#
.AddResilience(policy =>
{
policy.On<NpgsqlException>().Retry(5).ThenRedeliver();
policy.On<HttpRequestException>().Retry(3);
policy.Default().Retry().ThenRedeliver();
})

Bus-level policies

Bus-level policies apply to all endpoints and all consumers across the entire message bus.

C#
builder.Services
.AddMessageBus()
.AddResilience(policy =>
{
policy.On<ValidationException>().DeadLetter();
policy.On<DuplicateMessageException>().Discard();
policy.Default().Retry().ThenRedeliver();
})
.AddEventHandler<OrderPlacedHandler>()
.AddRabbitMQ();

Transport-level policies

Override bus-level policies for a specific transport. Transport-level policies replace the bus-level policies entirely for all endpoints on that transport - they are not merged.

C#
builder.Services
.AddMessageBus()
.AddResilience(policy =>
{
policy.Default().Retry().ThenRedeliver();
})
.AddRabbitMQ(transport =>
{
transport.AddResilience(policy =>
{
policy.On<Exception>().DeadLetter();
});
});

Consumer-level policies

Override policies for a specific consumer. Consumer-level policies replace the bus-level and transport-level policies for that consumer.

C#
builder.Services
.AddMessageBus()
.AddResilience(policy =>
{
policy.On<Exception>()
.Retry(2, TimeSpan.FromMilliseconds(100), RetryBackoffType.Constant);
});
builder.Services.ConfigureMessageBus(bus =>
{
bus.AddHandler<PaymentHandler>(consumer =>
{
consumer.AddResilience(policy =>
{
policy.On<Exception>()
.Retry(5, TimeSpan.FromMilliseconds(500), RetryBackoffType.Exponential);
});
});
});

The PaymentHandler gets 5 retries with exponential backoff. All other consumers get the bus-level policy of 2 retries.

Scope hierarchy

Exception policies resolve at four levels. The most specific scope wins, and replacement is atomic - the entire set of rules is replaced, not individual rules.

ScopeApplies toConfigured on
BusAll endpoints and consumersIMessageBusBuilder
HostAll message buses on the hostIMessageBusHostBuilder
TransportAll endpoints on a specific transportIReceiveMiddlewareProvider
ConsumerA single consumerIConsumerDescriptor
Consumer policies → Transport policies → Bus policies → Host policies
(highest priority) (lowest priority)

If a consumer defines exception policies, the bus-level and transport-level policies are ignored for that consumer. If you need a bus-level rule to also apply at the consumer level, include it in the consumer-level configuration.

Terminal actions

Terminal actions end the message's lifecycle immediately. No retry, no redelivery - the message is either routed to the error endpoint or discarded.

DeadLetter

DeadLetter() routes the message to the error endpoint, skipping both retry and redelivery. Use this for exceptions that are permanent - retrying will never succeed and you want the message preserved for inspection.

C#
policy.On<ValidationException>().DeadLetter();
policy.On<JsonException>().DeadLetter();
policy.On<UnauthorizedAccessException>().DeadLetter();

When to use: Malformed messages, authorization failures, schema violations, business rule violations that require manual intervention.

Discard

Discard() swallows the exception and the message disappears. No error endpoint, no fault headers, no trace beyond logging.

C#
policy.On<DuplicateMessageException>().Discard();
policy.On<MessageExpiredException>().Discard();

When to use: Messages that are safe to lose - duplicates you have already processed, stale events that no longer matter. Use with caution: discarded messages leave no audit trail in the error endpoint.

Retry policies

Retry re-runs the handler in-process using immediate retries. The message stays in memory, the concurrency slot is held, and the handler is invoked again after a short delay. Use retry for transient failures that resolve in milliseconds to seconds.

When you call .Retry() without chaining .ThenRedeliver(), redelivery is disabled for that exception type. If all retries are exhausted, the message routes to the error endpoint.

Retry with defaults

C#
policy.On<TimeoutException>().Retry();
SettingDefault value
Attempts3
Delay200 ms
BackoffExponential
JitterEnabled
Max delay30 seconds

Retry with custom attempts

C#
policy.On<NpgsqlException>(ex => ex.IsTransient).Retry(5);

Overrides the number of retry attempts. Delay, backoff strategy, jitter, and max delay use the built-in defaults.

Retry with full configuration

C#
policy.On<HttpRequestException>()
.Retry(3, TimeSpan.FromMilliseconds(500), RetryBackoffType.Exponential);

Overrides attempts, base delay, and backoff strategy for this exception type.

Retry with explicit intervals

C#
policy.On<SocketException>().Retry(
[
TimeSpan.FromMilliseconds(100),
TimeSpan.FromMilliseconds(500),
TimeSpan.FromSeconds(2)
]);

Specifies the exact delay before each retry attempt. The array length determines the number of retries.

Backoff strategies

StrategyBehavior
ConstantSame delay every attempt
Lineardelay * attempt
Exponentialdelay * 2^(attempt-1) (default)

All strategies apply jitter by default to prevent thundering herd effects.

Redelivery policies

Redelivery schedules the message for later delivery through the transport. The concurrency slot is released, the message re-enters the full receive pipeline on each redelivery attempt, and fresh retry cycles run on each delivery. Use redelivery for failures that need minutes or hours to resolve - a downstream service recovering from an outage, a rate limit resetting, or a database completing a failover.

When you call .Redeliver() directly (without .Retry() first), retry is disabled for that exception type. The handler failure goes straight to redelivery scheduling.

Redeliver with defaults

C#
policy.On<ExternalServiceException>().Redeliver();
SettingDefault value
Intervals5 min, 15 min, 30 min
JitterEnabled
Max delay1 hour

Redeliver with custom attempts and delay

C#
policy.On<RateLimitException>().Redeliver(5, TimeSpan.FromMinutes(2));

Overrides the number of attempts and base delay.

Redeliver with explicit intervals

C#
policy.On<ServiceUnavailableException>().Redeliver(
[
TimeSpan.FromSeconds(30),
TimeSpan.FromMinutes(5),
TimeSpan.FromMinutes(30)
]);

Specifies the exact delay before each redelivery attempt. The array length determines the number of redelivery attempts.

Escalation chains

The fluent API composes retry, redelivery, and terminal actions into escalation chains. The interface design enforces valid chains at compile time - you cannot chain .ThenRedeliver() after .Redeliver(), or .Retry() after .ThenRedeliver().

Retry then redeliver

C#
policy.On<NpgsqlException>(ex => ex.IsTransient)
.Retry(3)
.ThenRedeliver();

Try 3 immediate retries. If all fail, schedule redelivery with defaults (5, 15, 30 minutes). Each redelivery attempt runs a fresh cycle of 3 retries.

Retry then redeliver with custom settings

C#
policy.On<PaymentGatewayException>()
.Retry(5, TimeSpan.FromMilliseconds(500))
.ThenRedeliver(
[
TimeSpan.FromMinutes(5),
TimeSpan.FromMinutes(15),
TimeSpan.FromMinutes(30)
])
.ThenDeadLetter();

Try 5 immediate retries with 500 ms exponential backoff. If exhausted, schedule redeliveries at 5, 15, and 30 minutes. If all redeliveries are exhausted, route to the error endpoint.

Retry then dead-letter

C#
policy.On<ConcurrencyException>()
.Retry(3)
.ThenDeadLetter();

Try 3 immediate retries. If all fail, skip redelivery and route to the error endpoint immediately.

Chain behavior reference

ChainRetryRedeliveryTerminal
.Discard()--Discard
.DeadLetter()--DeadLetter
.Retry()EnabledDisabledDefault
.Retry(3)3 attemptsDisabledDefault
.Redeliver()DisabledEnabledDefault
.Retry(3).ThenRedeliver()3 attemptsEnabledDefault
.Retry(3).ThenDeadLetter()3 attemptsDisabledDeadLetter
.Retry(3).ThenRedeliver().ThenDeadLetter()3 attemptsEnabledDeadLetter

Default terminal behavior: When no terminal action is specified, exhausted messages route to the error endpoint through the fault middleware. .ThenDeadLetter() makes this intent explicit but does not change the behavior.

Disabled vs. default: "Disabled" means that tier is skipped for this exception type. A dash (-) means the tier is not configured and does not apply.

Conditional policies

Use predicate overloads to apply policies only when an exception matches specific conditions. The predicate receives the typed exception instance.

Filter by exception property

C#
// Only retry transient database errors
policy.On<NpgsqlException>(ex => ex.IsTransient)
.Retry(5);
// Dead-letter non-transient database errors
policy.On<NpgsqlException>(ex => !ex.IsTransient)
.DeadLetter();

Filter by HTTP status code

C#
policy.On<HttpRequestException>(ex =>
ex.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
.Redeliver(
[
TimeSpan.FromSeconds(30),
TimeSpan.FromMinutes(2),
TimeSpan.FromMinutes(10)
]);
policy.On<HttpRequestException>(ex =>
ex.StatusCode == System.Net.HttpStatusCode.BadRequest)
.DeadLetter();

Filter by inner exception

C#
policy.On<AggregateException>(ex =>
ex.InnerException is TimeoutException)
.Retry(3);

Multiple rules for the same type

You can define multiple rules for the same exception type with different predicates. When an exception is thrown, the first matching rule wins.

C#
policy.On<HttpRequestException>(ex =>
ex.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)
.Retry(3).ThenRedeliver();
policy.On<HttpRequestException>(ex =>
ex.StatusCode == System.Net.HttpStatusCode.BadRequest)
.DeadLetter();
// Catch-all for other HTTP errors
policy.On<HttpRequestException>().Retry(3);
Last updated on April 13, 2026 by Michael Staib