Error Handling
It’s an imperfect world and almost inevitable that your Wolverine message handlers will occasionally throw exceptions as message handling fails. Maybe because a piece of infrastructure is down, maybe you get transient network issues, or maybe a database is overloaded.
Wolverine comes with two flavors of error handling (so far). First, you can define error handling policies on message failures with fine-grained control over how various exceptions on different message. In addition, Wolverine supports a per-endpoint circuit breaker approach that will temporarily pause message processing on a single listening endpoint in the case of a high rate of failures at that endpoint.
Error Handling Rules
::: warning
When using IMessageBus.InvokeAsync() to execute a message inline, only the “Retry” and “Retry With Cooldown” error policies
are applied to the execution automatically. In other words, Wolverine will attempt to use retries inside the call to InvokeAsync() as
configured. Custom actions can be explicitly enabled for execution inside of InvokeAsync() as shown in a section below.
:::
Error handling rules in Wolverine are defined by three things:
- The scope of the rule. Really just per message type or global at this point.
- Exception matching
- One or more actions (retry the message? discard it? move it to an error queue?)
What to do on an error?
| Action | Description |
|---|---|
| Retry | Immediately retry the message inline without any pause |
| Retry with Cooldown | Wait a short amount of time, then retry the message inline |
| Requeue | Put the message at the back of the line for the receiving endpoint |
| Schedule Retry | Schedule the message to be retried at a certain time |
| Discard | Log, but otherwise discard the message and do not attempt to execute again |
| Move to Error Queue | Move the message to a dedicated dead letter queue and do not attempt to execute again |
| Pause the Listener | Stop all message processing on the current listener for a set duration of time |
While we think the options above will suffice for most scenarios, it’s possible to create your own action through Wolverine’s IContinuation interface.
So what to do in any particular scenario? Here’s some initial guidance:
- If the exception is a common, transient error like timeout conditions or database connectivity errors, build in a limited set of retries and potentially use exponential backoff to avoid overloading your system (sample of this below)
- If the exception tells you that the message is invalid or could never be processed, discard the message
- If an exception happens on multiple attempts, move the message to a “dead letter queue” where it might be possible to replay at some later time
- If an exception tells you than the system or part of the system itself is completely down, you may opt to pause the message listening altogether
Moving Messages to an Error Queue
::: tip The actual mechanics of the error or “dead letter queue” vary between messaging transport :::
By default, a message will be moved to an error queue when it exhausts all its configured retry/requeue slots dependent upon the exception filter. You can, however explicitly short circuit the retries and immediately send a message to the error queue like so:
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
// Don't retry, immediately send to the error queue
opts.OnException<TimeoutException>().MoveToErrorQueue();
}).StartAsync();snippet source | anchor
Discarding Messages
If you can detect that an exception means that the message is invalid in your system and could never be processed, just tell Wolverine to discard it:
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
// Bad message, get this thing out of here!
opts.OnException<InvalidMessageYouWillNeverBeAbleToProcessException>()
.Discard();
}).StartAsync();snippet source | anchor
You have to explicitly discard a message or it will eventually be sent to a dead letter queue when the message has exhausted its configured retries or requeues.
Exponential Backoff
::: tip This error handling strategy is effective for slowing down or throttling processing to give a distressed subsystem a chance to recover :::
Exponential backoff error handling is easy with either the RetryWithCooldown() syntax shown below:
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
// Retry the message again, but wait for the specified time
// The message will be dead lettered if it exhausts the delay
// attempts
opts
.OnException<SqlException>()
.RetryWithCooldown(50.Milliseconds(), 100.Milliseconds(), 250.Milliseconds());
}).StartAsync();snippet source | anchor
Or through attributes on a single message:
[RetryNow(typeof(SqlException), 50, 100, 250)]
public class MessageWithBackoff
{
// whatever members
}snippet source | anchor
Jitter
When many nodes retry at the same fixed delay after a shared downstream failure, they produce a “thundering herd” that pounds the recovering dependency in lockstep. Wolverine supports additive jitter on the three delay-based error policies: RetryWithCooldown, ScheduleRetry / ScheduleRetryIndefinitely, and PauseThenRequeue.
Invariant: jitter only extends the configured delay, never shortens it. The configured values remain the lower bound.
Three strategies are available. They are mutually exclusive per error rule.
Jitter is applied once per error rule — all slots in the rule share the same strategy, including those added via .Then. Attempting to call a second WithXxxJitter() method on the same rule (even after .Then) throws InvalidOperationException.
WithFullJitter
Effective delay ∈ [d, 2·d].
opts.OnException<DownstreamUnavailableException>()
.RetryWithCooldown(50.Milliseconds(), 100.Milliseconds(), 250.Milliseconds())
.WithFullJitter();WithBoundedJitter
Effective delay ∈ [d, d × (1 + percent)]. Useful when you deliberately picked the cooldown values and want to keep the spread narrow.
opts.OnException<DownstreamUnavailableException>()
.ScheduleRetry(1.Seconds(), 5.Seconds(), 30.Seconds())
.WithBoundedJitter(0.25); // +0% to +25%percent must be greater than zero; there is no upper bound.
WithExponentialJitter
Effective delay ∈ [d, d × (1 + 2·attempt)]. The spread widens with every attempt, so persistent failures fan out more than transient ones. This is an attempt-scaled, stateless variant of the “decorrelated jitter” pattern — it deliberately avoids persisting the previous actual delay per envelope.
opts.OnException<DownstreamUnavailableException>()
.PauseThenRequeue(5.Seconds())
.WithExponentialJitter();Pausing Listening on Error Conditions
::: tip This feature exists in Wolverine because of the exact scenario described as an example in this section. Wish we’d had Wolverine then… :::
A common usage of asynchronous messaging frameworks is to make calls to an external API as a discrete step within a discrete message handler to isolate the calls to that external API from the rest of your application and put those calls into its own, isolated retry loop in the case of failures. Great! But what if something happens to that external API such that it’s completely unable to accept any requests without manual intervention? You don’t want to keep retrying messages that will just fail and eventually land in a dead letter queue where they can’t be easily retried without manual intervention.
Instead, let’s just tell Wolverine to immediately pause all message processing in the incoming message listener when a certain exception is detected like so:
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
// The failing message is requeued for later processing, then
// the specific listener is paused for 10 minutes
opts.OnException<SystemIsCompletelyUnusableException>()
.Requeue().AndPauseProcessing(10.Minutes());
}).StartAsync();snippet source | anchor
Scoping
::: tip To be clear, the error rules are “fall through,” meaning that the rules are evaluated in order. :::
In order of precedence, exception handling rules can be defined at either the specific message type or globally. As a third possibility, you can use a chain policy to specify exception handling rules with any kind of user defined logic — usually against a subset of message types.
::: tip The Wolverine team recommends using one style (attributes or fluent interface) or another, but not to mix and match styles too much within the same application so as to make reasoning about the error handling too difficult. :::
First off, you can define error handling rules for a specific message type by placing attributes on either the handler method or the message type itself as shown below:
public class AttributeUsingHandler
{
[ScheduleRetry(typeof(IOException), 5)]
[RetryNow(typeof(SqlException), 50, 100, 250)]
[RequeueOn(typeof(InvalidOperationException))]
[MoveToErrorQueueOn(typeof(DivideByZeroException))]
[MaximumAttempts(2)]
public void Handle(InvoiceCreated created)
{
// handle the invoice created message
}
}snippet source | anchor
You can also use the fluent interface approach on a specific message type if you put a method with the signature public static void Configure(HandlerChain chain)
on the handler class itself as in this sample:
public class MyErrorCausingHandler
{
// This method signature is meaningful
public static void Configure(HandlerChain chain)
{
// Requeue on IOException for a maximum
// of 3 attempts
chain.OnException<IOException>()
.Requeue();
}
public void Handle(InvoiceCreated created)
{
// handle the invoice created message
}
public void Handle(InvoiceApproved approved)
{
// handle the invoice approved message
}
}snippet source | anchor
To specify global error handling rules, use the fluent interface directly on WolverineOptions.Handlers as shown below:
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.Policies.OnException<TimeoutException>().ScheduleRetry(5.Seconds());
opts.Policies.OnException<SecurityException>().MoveToErrorQueue();
// You can also apply an additional filter on the
// exception type for finer grained policies
opts.Policies
.OnException<SocketException>(ex => ex.Message.Contains("not responding"))
.ScheduleRetry(5.Seconds());
}).StartAsync();snippet source | anchor
TODO — link to chain policies, after that exists:)
Lastly, you can use chain policies to add error handling policies to a selected subset of message handlers. First, here’s
a sample policy that applies an error handling policy based on SqlException errors for all message types from a certain namespace:
// This error policy will apply to all message types in the namespace
// 'MyApp.Messages', and add a "requeue on SqlException" to all of these
// message handlers
public class ErrorHandlingPolicy : IHandlerPolicy
{
public void Apply(IReadOnlyList<HandlerChain> chains, GenerationRules rules, IServiceContainer container)
{
var matchingChains = chains
.Where(x => x.MessageType.IsInNamespace("MyApp.Messages"));
foreach (var chain in matchingChains) chain.OnException<SqlException>().Requeue(2);
}
}snippet source | anchor
Exception Filtering
::: tip
While many of the examples in this page have shown simple policies based on the type SqlException, in real life
you would probably want to filter on specific error codes to fine tune your error handling for SQL failures that
are transient versus failures that imply the message could never be processed.
:::
The attributes are limited to exception type, but the fluent interface has quite a few options to filter exception further with additional filters, inner exception tests, and compound filters:
sample_filtering_by_exception_type
Custom Actions
::: tip For the sake of granular error handling, it’s recommended that your custom error handler code limit itself to publishing additional messages rather than trying to do work inline :::
Wolverine will enable you to create custom exception handling actions as additional steps to take during message failures.
As an example, let’s say that when your system is sent a ShipOrder message you’d like to send the original
sending service a corresponding ShippingFailed message when that ShipOrder message fails during processing.
The following code shows how to do this with an inline function:
theReceiver = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.ListenAtPort(receiverPort);
opts.ServiceName = "Receiver";
opts.Policies.OnException<ShippingFailedException>()
.Discard().And(async (_, context, _) =>
{
if (context.Envelope?.Message is ShipOrder cmd)
{
await context.RespondToSenderAsync(new ShippingFailed(cmd.OrderId));
}
});
}).StartAsync();snippet source | anchor
Optionally, you can implement a new type to handle this same custom logic by
subclassing the Wolverine.ErrorHandling.UserDefinedContinuation type like so:
public class ShippingOrderFailurePolicy : UserDefinedContinuation
{
public ShippingOrderFailurePolicy() : base(
$"Send a {nameof(ShippingFailed)} back to the sender on shipping order failures")
{
}
public override async ValueTask ExecuteAsync(IEnvelopeLifecycle lifecycle, IWolverineRuntime runtime,
DateTimeOffset now, Activity? activity)
{
if (lifecycle.Envelope?.Message is ShipOrder cmd)
{
await lifecycle
.RespondToSenderAsync(new ShippingFailed(cmd.OrderId));
}
}
}snippet source | anchor
and register that secondary action like this:
theReceiver = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.ListenAtPort(receiverPort);
opts.ServiceName = "Receiver";
opts.Policies.OnException<ShippingFailedException>()
.Discard().And<ShippingOrderFailurePolicy>();
}).StartAsync();snippet source | anchor
Circuit Breaker
::: tip At this point, the circuit breaker mechanics need to be applied on an endpoint by endpoint basis :::
Wolverine also supports a circuit breaker strategy for handling errors. The purpose of a circuit breaker is to pause message handling for a single endpoint if there are a significant percentage of message failures in order to allow the system to catch up and possibly allow for a distressed subsystem to recover and stabilize.
The usage of the Wolverine circuit breaker is shown below:
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.Policies.OnException<InvalidOperationException>()
.Discard();
opts.ListenToRabbitQueue("incoming")
.CircuitBreaker(cb =>
{
// Minimum number of messages encountered within the tracking period
// before the circuit breaker will be evaluated
cb.MinimumThreshold = 10;
// The time to pause the message processing before trying to restart
cb.PauseTime = 1.Minutes();
// The tracking period for the evaluation. Statistics tracking
cb.TrackingPeriod = 5.Minutes();
// If the failure percentage is higher than this number, trip
// the circuit and stop processing
cb.FailurePercentageThreshold = 10;
// Optional allow list
cb.Include<NpgsqlException>(e => e.Message.Contains("Failure"));
cb.Include<SocketException>();
// Optional ignore list
cb.Exclude<InvalidOperationException>();
});
}).StartAsync();snippet source | anchor
Note that the exception includes and excludes are optional. If there are no explicit Include()
calls, the circuit breaker will assume that every exception should be considered a failure.
Likewise, if there are no Exclude() calls, the circuit breaker will not throw out any
exceptions. Also note that it probably makes no sense to define both Include() and Exclude()
rules.
Custom Actions for InvokeAsync()
::: info
This usage was built for a JasperFx Software customer who is using Wolverine by calling IMessageBus.InvokeAsync()
directly underneath Hot Chocolate mutations. In their case, if the
mutation action failed more than X number of times, they wanted to send a different message that would try to jumpstart the long running
workflow that is somehow stalled.
:::
This is maybe a little specialized, but let’s say you have a reason for calling IMessageBus.InvokeAsync() inline, and
that you want to carry out some kind of custom action if the message handler exceeds a certain number of retries (the only
error handling action that applies automatically to InvokeAsync()). You can now opt custom actions into applying to
exceptions thrown by your message handlers during a call to InvokeAsync() by specifying an InvokeResult value of Stop
or TryAgain to a custom action. Here’s a sample that uses a CompensatingAction() helper method for raising other messages
on failures:
public record ApproveInvoice(string InvoiceId);
public record RequireIntervention(string InvoiceId);
public static class InvoiceHandler
{
public static void Configure(HandlerChain chain)
{
chain.OnAnyException().RetryTimes(3)
.Then
.CompensatingAction<ApproveInvoice>((message, ex, bus) => bus.PublishAsync(new RequireIntervention(message.InvoiceId)),
// By specifying a value here for InvokeResult, I'm making
// this action apply to failures inside of IMessageBus.InvokeAsync()
InvokeResult.Stop);
// This is just a long hand way of doing the same thing as CompensatingAction
// .CustomAction(async (runtime, lifecycle, _) =>
// {
// if (lifecycle.Envelope.Message is ApproveInvoice message)
// {
// var bus = new MessageBus(runtime);
// await bus.PublishAsync(new RequireIntervention(message.InvoiceId));
// }
//
// }, "Send a compensating action", InvokeResult.Stop);
}
public static int SucceedOnAttempt = 0;
public static void Handle(ApproveInvoice invoice, Envelope envelope)
{
if (envelope.Attempts >= SucceedOnAttempt) return;
throw new Exception();
}
public static void Handle(RequireIntervention message)
{
Debug.WriteLine($"Got: {message}");
}
}snippet source | anchor
Running custom actions indefinitely
In some scenarios you want your custom action to control the retry lifecycle across multiple attempts (e.g., reschedule with a delay until some external condition is met), instead of Wolverine moving the message to the error queue after the first attempt. For that, use CustomActionIndefinitely(...).
CustomActionIndefinitely keeps invoking your custom action on subsequent attempts until your code explicitly stops the process. Inside the delegate you can for example:
- Reschedule the message (e.g., with backoff, or by some dynamic values based on exception’s payload…) via
lifecycle.ReScheduleAsync(...) - Requeue if appropriate
- Or stop further processing by calling
lifecycle.CompleteAsync()(optionally after logging or publishing a compensating message)
Example:
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.Policies
.OnException<SpecialException>()
.CustomActionIndefinitely(async (runtime, lifecycle, ex) =>
{
// Stop after 10 attempts
if (lifecycle.Envelope.Attempts >= 10)
{
// Decide to stop trying; you could also move to an error queue
await lifecycle.CompleteAsync();
return;
}
// Keep trying later with a delay
await lifecycle.ReScheduleAsync(DateTimeOffset.UtcNow.AddSeconds(15));
}, "Handle SpecialException with conditional reschedule/stop");
}).StartAsync();Note that custom actions would always be applied to exceptions thrown in asynchronous message handling.
Fault Events
Wolverine ships an opt-in mechanism that auto-publishes a strongly-typed
Fault<T> envelope whenever a handler for T terminally fails. Use it
when distributed consumers want to react to failures programmatically,
or when a typed projection of “what failed and why” is more useful than
inspecting a generic dead-letter queue.
Fault events sit after your retry / requeue / DLQ rules above — they fire when a message has reached a terminal state per those rules, not before.
Quickstart
Opt-in globally:
opts.PublishFaultEvents();Subscribe with a normal Wolverine handler — no special attribute, no opt-in registration:
public static class OrderPlacedFaultHandler
{
public static void Handle(Fault<OrderPlaced> fault) =>
Console.WriteLine($"Order {fault.Message.Id} failed: {fault.Exception.Message}");
}Whenever OrderPlacedHandler fails terminally (retries exhausted, moved
to error queue, or — if opted in — discarded), Wolverine publishes a
Fault<OrderPlaced> envelope through the global routing graph.
Anatomy of Fault<T>
public record Fault<T>(
T Message,
ExceptionInfo Exception,
int Attempts,
DateTimeOffset FailedAt,
string? CorrelationId,
Guid ConversationId,
string? TenantId,
string? Source,
IReadOnlyDictionary<string, string?> Headers
) where T : class;Message— the original failing message, exactly as it was deserialized.Exception—ExceptionInforecord withType,Message,StackTrace,InnerException. Inner-exception recursion is depth-capped at 10 to bound payload size.Attempts— how many delivery attempts the failing envelope went through before the terminal decision.FailedAt— the timestamp of the terminal decision (UTC).CorrelationId,ConversationId,TenantId,Source— propagated from the failing envelope.Sourceis the original sender’s identity, not the fault publisher’s.Headers— copied from the failing envelope withwolverine.encryption.*headers stripped (encryption is decided fresh on the outbound fault hop).
The static FaultHeaders class exposes three constants set on every
auto-published fault envelope:
public static class FaultHeaders
{
public const string AutoPublished = "wolverine.fault.auto";
public const string OriginalId = "wolverine.fault.original_id";
public const string OriginalType = "wolverine.fault.original_type";
}AutoPublished is set to "true" on auto-published faults only.
Hand-published faults (bus.PublishAsync(new Fault<T>(...))) do not
carry this header — useful for distinguishing the two in subscribers
and tests. OriginalId and OriginalType carry the failing envelope’s
ID and Wolverine message-type name so trace consumers can correlate
without inspecting the fault body.
Delivery semantics and scope
A fault is published when:
- Moved to error queue (DLQ) — every retry policy that ends in DLQ.
- Discarded — only when the failure rule was configured with
discardWithFaultPublish: true. - Expired envelope — handler entry observes the envelope past its
DeliverBy; counts as a terminal failure.
A fault is not published in these bypass paths:
- Send-side failures — the broker rejects the outbound publish before it ever reached a handler.
- Unknown message type at the receiver — Wolverine cannot synthesize a
Tto wrap. - Pre-handler crypto failures —
EncryptionPolicyViolationException,EncryptionMissingHeaderException,EncryptionDecryptionExceptionshort-circuit before the handler runs. - Fault-publish recursion — a failing
Fault<T>handler will not trigger aFault<Fault<T>>. The recursion guard logs at Debug and emits awolverine.fault.recursion_suppressedactivity event.
Atomicity caveat. Fault publish is best-effort, not transactionally co-committed with the DLQ insert. The receive-side outbox does not enrol the fault publish in the same transaction as the DLQ row. In the unlikely window where the DLQ commit succeeds but the fault enqueue throws, the fault is dropped (logged at Error and the failure counter is incremented). Subscribers must therefore be resilient to gaps; they cannot use Fault events as a strict audit log.
Subscribing to faults
Standard handler discovery applies — write a method named Handle /
HandleAsync / Consume / ConsumeAsync taking Fault<T> for each
T you care about. Routing for the fault envelope uses the global
routing graph; persistence uses the same outbox/inbox you configured
for any other message.
A test-friendly subscriber distinguishes auto-published from hand-published faults:
public static class OrderPlacedFaultHandler
{
public static void Handle(Fault<OrderPlaced> fault, Envelope envelope)
{
var auto = envelope.Headers.TryGetValue(FaultHeaders.AutoPublished, out var v)
&& v == "true";
Console.WriteLine($"Order {fault.Message.Id} {(auto ? "auto-faulted" : "manually faulted")}");
}
}Naming convention. Wolverine’s conventional handler discovery requires class names ending in
HandlerorConsumer(or[WolverineHandler]on the class). A class namedOrderPlacedFaultSinkwill not be discovered automatically.
Per-type fault configuration
Override the global mode and redaction on a single message type:
opts.Policies.ForMessagesOfType<OrderPlaced>()
.PublishFault(includeExceptionMessage: true, includeStackTrace: false);
opts.Policies.ForMessagesOfType<HighVolumeChatter>()
.DoNotPublishFault();Override semantics:
- A per-type override is fully specified — Mode and redaction never partially inherit from globals. Calling
PublishFault()with no parameters setsincludeExceptionMessage = true, includeStackTrace = true(the parameter defaults), even ifPublishFaultEvents(includeExceptionMessage: false)was set globally. Always pass the redaction flags explicitly when overriding. - Calls must happen before host startup.
WolverineRuntime.StartAsynccallsFaultPublishingPolicy.Freeze(); later attempts to add or change overrides throwInvalidOperationException.
Fault redaction
Two flags on PublishFaultEvents (global) and matching parameters on
PublishFault (per-type):
opts.PublishFaultEvents(
includeExceptionMessage: false,
includeStackTrace: false);What gets redacted:
Fault<T>.Exception.Message→string.EmptyFault<T>.Exception.StackTrace→null- Recurses through
InnerExceptionandAggregateException.InnerExceptions. ExceptionInfo.Typeis always preserved (type names are in source code anyway).- Headers,
Source, correlation/conversation/tenant IDs are never redacted.
Note: redaction targets
Fault<T>.Exceptiononly. The original message instanceTcarried asFault<T>.Messageis unchanged — that is what fault events are for. IfTitself is sensitive, the per-type encryption pairing (next section) is the right tool.
Fault encryption pairing
Calling Policies.ForMessagesOfType<T>().Encrypt() automatically
registers the encrypting serializer rule for Fault<T> and adds
typeof(Fault<T>) to the receive-side encryption requirement set. No
manual setup. Skipped for value-type T because Fault<T> requires
T : class.
See Message Encryption → Fault events
for the byte-level mechanics, the wolverine.encryption.* header
strip, and the receive-side RequireEncryption() interaction. Note in
particular that RequireEncryption() is a receive-side guard only —
it does not constrain the outbound republish of an auto-published
Fault<T> triggered by failures on that listener.
Fault observability
Three Activity events are added to the failing envelope’s span:
wolverine.fault.published— when a fault is enqueued for routing.wolverine.fault.no_route— when no route exists forFault<T>. Tagged withwolverine.fault.message_type.wolverine.fault.recursion_suppressed— when the recursion guard fires.
One counter:
wolverine.fault.events_published—Counter<int>, incremented per fault enqueued. Suppressed (recursion-guarded) faults do not increment this counter.
On publish failure (the MUST NOT throw contract): the publisher
catches, logs at Error, sets the activity status to Error, and emits a
wolverine.fault.publish_failed activity event.
Outbound Fault<T> envelopes inherit ConversationId, CorrelationId,
and TraceParent from the failing envelope, so distributed traces stay
connected across the failure → fault hop.
Testing fault events with ITrackedSession
The tracked-session API surfaces auto-published faults so test assertions don’t have to subscribe explicitly:
var tracked = await host.TrackActivity()
.DoNotAssertOnExceptionsDetected()
.SendMessageAndWaitAsync(new OrderPlaced(...));
var faults = tracked.AutoFaultsPublished.OfType<Fault<OrderPlaced>>().ToArray();
faults.ShouldHaveSingleItem();Hand-published bus.PublishAsync(new Fault<T>(...)) calls do not
appear in AutoFaultsPublished — only auto-header’d ones do. This lets
tests distinguish unintended auto-publishes from intentional manual
ones.
Fault event pitfalls
Fault<T>.ToString()leaksMessageplaintext. Positional records auto-generate aToStringthat includes every field. Logging$"Got {fault}"writes the wrappedTplaintext into your log sink. For sensitiveT, use the encryption pairing AND avoid logging the fault directly.RequireEncryption()does not constrain outbound faults. Marking a listener.RequireEncryption()only rejects unencrypted inbound envelopes on that listener; it has no effect on whether aFault<T>triggered by a failure on that listener is encrypted on its outbound hop. Use the per-typeEncrypt()pairing for outbound protection.- Manual
bus.PublishAsync(new Fault<T>(...))skips the auto-header. That is by design — assertions andITrackedSession.AutoFaultsPublisheddistinguish auto from manual. Filtering onFaultHeaders.AutoPublishedexcludes manual publishes. - Recursion suppression is silent in metrics. A suppressed recursive fault does NOT increment
wolverine.fault.events_published. Watch for thewolverine.fault.recursion_suppressedactivity event if you suspect a Fault-handler is faulting. - Per-type override defaults are NOT global defaults.
Policies.ForMessagesOfType<T>().PublishFault()with no parameters uses the parameter defaults (true/true), independent of the globalPublishFaultEvents(...)redaction settings. Always pass the redaction flags explicitly when overriding.
See also
- Message Encryption → Fault events — byte-level encryption interaction.
FaultEventsDemo— runnable single-process sample.