Error Handling
Default Behavior
When a handler throws and no error handler is registered, the framework:
- Logs the error with full context (message ID, type, entity name)
- Re-throws the exception so the Azure Service Bus SDK abandons the message (returns it to the queue for retry)
- After
MaxDeliveryCountfailed attempts, the Service Bus moves the message to the dead-letter sub-queue automatically
AutoComplete and abandonment
With the default AutoComplete = true setting, unhandled exceptions must propagate back to the SDK for the abandon to happen. The framework ensures this by re-throwing after logging when no error handler settles the message. If you configure AutoComplete = false, the framework calls AbandonAsync explicitly instead.
NOTE
Azure Service Bus configuration: The number of delivery attempts before dead-lettering is controlled by the MaxDeliveryCount property on the queue or subscription (default: 10). This is set at entity creation time but can be updated later.
resource ordersQueue 'Microsoft.ServiceBus/namespaces/queues@2022-10-01-preview' = {
name: 'orders'
properties: {
maxDeliveryCount: 5 // Dead-letter after 5 failed attempts
deadLetteringOnMessageExpiration: true // Also dead-letter expired messages
}
}Dead-letter sub-queues are created automatically by Azure Service Bus - you cannot disable them. Each queue and subscription gets its own dead-letter sub-queue at <entity>/$deadletterqueue.
Custom Error Handling
messaging.MapQueue<OrderCreated>("orders")
.MapHandler(handler)
.MapErrorHandler(async (exception, context, ct) =>
{
if (exception is ValidationException)
{
// Poison message - dead letter immediately
await context.DeadLetterAsync("ValidationFailed", exception.Message, ct);
return;
}
if (exception is TransientException && context.DeliveryCount < 5)
{
// Retry
await context.AbandonAsync(null, ct);
return;
}
// Give up
await context.DeadLetterAsync("ProcessingFailed", exception.ToString(), ct);
});Global Error Handler
var messaging = builder.Services.AddMessaging(options =>
{
options.OnProcessingError = async (exception, context, ct) =>
{
// Global logging, alerting, etc.
};
});Dead Letter Processing
// Process dead letters from a queue
messaging.MapQueue<OrderCreated>("orders")
.MapDeadLetterHandler(handler);
// Or with full context
messaging.MapQueue<OrderCreated>("orders")
.MapDeadLetterHandler(async (
OrderCreated message,
DeadLetterContext dlContext,
CancellationToken ct) =>
{
Console.WriteLine($"Dead letter reason: {dlContext.DeadLetterReason}");
Console.WriteLine($"Error description: {dlContext.DeadLetterErrorDescription}");
// Resubmit, archive, alert, etc.
});
// Process dead letters from a subscription
messaging.MapTopic<OrderCreated>("order-events")
.MapDeadLetterSubscription("audit", handler);DeadLetterContext exposes DeadLetterReason, DeadLetterErrorDescription, and DeadLetterSource for diagnosing why a message was dead-lettered.
Health Checks
builder.Services.AddHealthChecks()
.AddServiceBusCheck("orders-queue", tags: ["ready"])
.AddServiceBusCheck("payments", "billing-service", tags: ["ready"]);