Skip to main content
Version: Next (unreleased)

Middleware & Error Handling

Middleware wraps the message handler, executing in order (outermost first):

consConfig := rabbitmq.DefaultConsumerConfig().
WithQueue("my-queue").
WithMiddleware(
rabbitmq.LoggingMiddleware(rabbitmq.NewStdLogger()),
rabbitmq.RecoveryMiddleware(func(r any) {
log.Printf("recovered from panic: %v", r)
}),
rabbitmq.RetryMiddleware(3, 1*time.Second),
)

Built-in Middleware

MiddlewareDescription
LoggingMiddleware(logger)Logs message processing with duration
RecoveryMiddleware(onPanic)Recovers from panics in handlers
RetryMiddleware(maxRetries, delay)Retries failed processing in-process (short waits)
BackoffRetryMiddleware(pub, queue, maxRetries, base)Retries at the broker with exponential backoff, freeing the slot — see Dead-Letter Queues

Custom Middleware

func TracingMiddleware(tracer Tracer) rabbitmq.Middleware {
return func(next rabbitmq.MessageHandler) rabbitmq.MessageHandler {
return func(ctx context.Context, d *rabbitmq.Delivery) error {
span := tracer.StartSpan("process_message")
defer span.End()
return next(ctx, d)
}
}
}

Composing Middleware

combined := rabbitmq.Chain(mw1, mw2, mw3)
handler := combined(myHandler)

Error Handling

When a handler returns an error, the message is nacked. By default (RequeueOnError: false) it is not requeued — it is dead-lettered if a dead-letter exchange is configured, otherwise discarded. This avoids a poison message hot-looping. Opt into unconditional requeue with WithRequeueOnError(true).

consConfig := rabbitmq.DefaultConsumerConfig().
WithQueue("my-queue").
WithErrorHandler(func(err error) {
log.Printf("Consumer error: %v", err)
})

For per-message control, return a sentinel error from the handler — it overrides the RequeueOnError default and may be wrapped with %w:

err = consumer.Consume(ctx, func(ctx context.Context, d *rabbitmq.Delivery) error {
if err := process(d); err != nil {
if isTransient(err) {
return fmt.Errorf("temporary: %w", rabbitmq.ErrRequeue) // requeue and retry
}
return fmt.Errorf("poison: %w", rabbitmq.ErrDrop) // never requeue (dead-letter/discard)
}
return nil
})

:::note RetryMiddleware vs. the RequeueOnError default

RetryMiddleware retries happen in-process (the handler goroutine and its prefetch slot are held for the delay), so it suits short retries, not long backoff. After the retries are exhausted the error is nacked per the rules above — so with the default it is dead-lettered. Combining it with RequeueOnError(true) (without returning ErrDrop) reintroduces an unbounded retry loop.

:::

See Errors for the full list of sentinel errors and how to match them with errors.Is.