Error Handling
Consumer failure behavior is explicit, choose what happens to the message when a handler throws.
Rabbit Relay makes consumer failure behavior explicit.
When a handler throws, you choose what happens to the message.
RabbitMQ acknowledgements
RabbitMQ uses acknowledgements to know whether a delivered message has been handled successfully.
| Term | Meaning |
|---|---|
ACK | The message was handled and RabbitMQ can remove it from the queue |
NACK requeue=true | The message was rejected and should be put back on the queue |
NACK requeue=false | The message was rejected and should not be requeued |
| Dead-letter | RabbitMQ routes a rejected message to a configured DLQ |
Rabbit Relay default
If a handler succeeds, Rabbit Relay ACKs the message.
If a handler throws, Rabbit Relay follows the onError policy.
Error modes
await sub.consume({
onError: "ack" | "requeue" | "dead-letter" | "retry",
});onError | RabbitMQ behavior | Use case |
|---|---|---|
"ack" | ACK even after handler error | Non-critical events, logs, metrics |
"requeue" | NACK requeue=true | Transient failures, use carefully |
"dead-letter" | NACK requeue=false | Poison messages, validation errors |
"retry" | Publish retry copy, then ACK original | Production consumers |
ack (default)
await sub.consume({
onError: "ack",
});Behavior:
- handler errors are logged
- message is acknowledged
- no retry occurs
Use for non-critical events, logs, metrics, or handlers where failure should not block the queue.
Data loss risk
onError: "ack" drops failed messages. Use it only when that is acceptable.
requeue
await sub.consume({
onError: "requeue",
});Behavior:
- message is negatively acknowledged
- message is requeued
- RabbitMQ may redeliver it immediately
Infinite loop risk
Do not use requeue as your main retry strategy. If the error is not transient, the same message can be delivered repeatedly forever.
dead-letter
await sub.consume({
onError: "dead-letter",
});Behavior:
- message is negatively acknowledged
- requeue is disabled
- RabbitMQ routes the message to the configured DLQ
Use for poison messages, validation errors, and failures that need inspection.
retry
await sub.consume({
onError: "retry",
retry: {
attempts: 3,
then: "dead-letter",
},
});Behavior:
- Rabbit Relay republishes the message for another attempt
- retry metadata is stored in headers
- after max attempts, the final behavior is applied
Common final behavior:
then: "dead-letter"Production default
For production consumers, prefer bounded retry followed by DLQ.
Why Rabbit Relay ACKs the original after retry publish
When retry is enabled, Rabbit Relay does not leave the original message unacked forever.
Instead:
- It republishes a retry copy with retry headers
- It ACKs the original only after the retry copy is published
- RabbitMQ later delivers the retry copy
This avoids infinite immediate redelivery loops and keeps retry metadata explicit.
Recommended production pattern
const sub = await broker
.queue("orders.q")
.exchange("orders.ex", {
exchangeType: "topic",
routingKey: "orders.*",
deadLetter: {
exchange: "orders.dlx",
queue: "orders.dlq",
routingKey: "orders.dead",
autoDeclare: true,
},
});
await sub.consume({
prefetch: 20,
concurrency: 5,
onError: "retry",
retry: {
attempts: 3,
then: "dead-letter",
},
});This gives you:
- bounded retries
- no infinite requeue loop
- failed messages isolated in a DLQ
- visible retry metadata
At-least-once delivery
RabbitMQ delivery remains at-least-once.
A message can be delivered more than once if:
- a consumer crashes before ACK
- the connection drops during processing
- a retry or redrive publishes another copy
- a publisher retries after an uncertain failure
Design handlers to be idempotent
Use stable IDs, unique constraints, de-duplication, or idempotent writes where duplicate processing would be harmful.
Summary
ackdrops failed messagesrequeuesends them back immediatelydead-letterparks them in a DLQretryretries a bounded number of times- Prefer retry + DLQ for production consumers