Skip to content

Error Handling

Default Behavior

When a handler throws and no error handler is registered, the framework:

  1. Logs the error with full context (message ID, type, entity name)
  2. Re-throws the exception so the Azure Service Bus SDK abandons the message (returns it to the queue for retry)
  3. After MaxDeliveryCount failed 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.

bicep
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

csharp
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

csharp
var messaging = builder.Services.AddMessaging(options =>
{
    options.OnProcessingError = async (exception, context, ct) =>
    {
        // Global logging, alerting, etc.
    };
});

Dead Letter Processing

csharp
// 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

csharp
builder.Services.AddHealthChecks()
    .AddServiceBusCheck("orders-queue", tags: ["ready"])
    .AddServiceBusCheck("payments", "billing-service", tags: ["ready"]);