For the complete documentation index, see llms.txt. This page is also available as Markdown.

Error Channel

Error channels and dead letter queues for failed message handling

A handler throws after three retries. Without somewhere to put the failed message, you have two options: lose it or stop the consumer. The Error Channel is the third option — a holding place for failed messages so you can fix the bug, replay them, and keep processing the rest.

The Error Channel is just another channel; what happens to messages depends on the handler you connect to it: log them, store them in a database table, push them to another async channel for later replay, or send them to an alerting system.

Error Channel Flow

On the high level Error Channel works as follows:

Message is sent to Error Channel after failing
  1. Message Consumer is polling Messages from the Queue and executing related Message Handlers.

  2. When execution of given Handler fails, Error is propagated back to Message Consumer

  3. Message Consumer based on the configuration sends it to related Error Channel

Configuration

Error Channel can be configured per Message Consumer, or globally as default Error Channel for all Message Consumers:

- Symfony

- Laravel

- Lite

config/packages/ecotone.yaml

config/ecotone.php

Setting up Error Channel means that Message Consumer will send Error Message to error channel and then continue handling next messages. After sending Error Message to Error Channel, message is considered handled as long as Error Handler does not throw exception.

Handling Error Messages

Manual Handling

To handle incoming Error Messages, we can bind to our defined Error Channel using ServiceActivator:

Internal Handlers are endpoints like Command Handlers, however they are not exposed using Command/Event/Query Buses. You may use them for internal handling.

Delayed Retries

Ecotone provides inbuilt retry mechanism, in case of failure Error Message will be resent to its original Message Channel with a delay. This way we will give application a chance to self-heal and return to good state.

Using inbuilt retry mechanism to resend Message with delay

To configure Delayed Retries we need to set up Error Configuration and connect it to our Error Channel:

Delayed Retries require an asynchronous Message Channel as the source.

The retry mechanism reschedules the failed Message back onto the originating pollable Channel with a DELIVERY_DELAY. That requires the Message to have come from an async Message Channel (queue/database channel).

For sources that don't have a pollable channel to retry into — synchronous Command Bus invocations, or inbound Channel Adapters like #[KafkaConsumer]ErrorHandlerConfiguration will throw MessageHandlingException with a clear message about the missing origination channel. For those use cases prefer:

  • Synchronous gateway — combine #[ErrorChannel] with #[InstantRetry] on the gateway, or route the Error Channel to an async Message Channel.

  • Inbound Channel Adapter (Kafka, AMQP inbound, #[Scheduled]) — route the Error Channel directly to a Dead Letter (e.g. dbal_dead_letter) without a delayed retry template; the failed Message can be replayed from the Dead Letter once the underlying problem is fixed.

Discarding all Error Messages

If for some cases we want to discard Error Messages, we can set up error channel to default inbuilt one called "nullChannel". That may be used in combination of retries, if after given attempt Message is still not handled, then discard:

Dbal Dead Letter

Ecotone comes with full support for managing full life cycle of a error message. This allows us to store Message in database for later review. Then we can review the Message, replay it or delete.

Using Dead Letter for storing Error Message

Read more in next section.

Command Bus Error Channel

Route failed synchronous commands to dedicated error handling with a single #[ErrorChannel] attribute. Instead of catching exceptions in each handler and manually routing to error handling, declare the error channel once. Failed messages are automatically routed for retry, logging, or dead-letter processing.

You'll know you need this when:

  • Failed commands need specific error handling: alerting, manual review, or audit trails

  • Payment or financial operations require failure tracking for compliance

  • You receive webhooks and need to handle failures gracefully instead of throwing exceptions

  • Scattered try/catch blocks in handlers are becoming unmanageable

  • Different command categories need different error handling strategies

Command Bus with Error Channel

To set up Error Channel for Command Bus, we will extend Command Bus with our Interface and add ErrorChannel attribute.

Command Bus with Dead Letter

Now instead of using CommandBus, we will be using ResilientCommandBus for sending Commands. Whenever failure will happen, instead being propagated, it will now will be redirected to our Dead Letter and stored in database for later review.

Command Bus with Error Channel and Instant Retry

We can extend our Command Bus with Error Channel by providing instant retries. This way we can do automatic retries before we will consider Message as failed and move it to the Error Channel. This way we give ourselves a chance of self-healing automatically in case of transistent errors, like database or network exceptions.

Now instead of using CommandBus, we will be using ResilientCommandBus for sending Commands. Whenever failure will happen, instead being propagated, it will now will be redirected to our Dead Letter and stored in database for later review.

Command Bus with Asynchronous Error Channel

Instead of pushing Message to Error Channel, we can push it to Asynchronous Message Channel from which Message will be consumed and retried again. This way in case of failure we can make it possible for Message to be retried and end up self-healing.

Command Bus with Asynchronous Error Channel

and then for use RabbitMQ Message Channel:

Command Bus with Asynchronous Error Channel and Delayed Retries

We can combine Asynchronous Error Channel together with delayed retries, creating robust solution, that our Application is able to self-heal from transistent errors even if they take some period of time. For example if our calling some external Service fails, or database went down, then we may receive the same error when Message is retrieved by Async Channel. However if we will delay that by 20 seconds, then there is huge chance that everything will get back on track, and the Application will self-heal automatically.

Command Bus with Asynchronous Error Channel and delayed retries

Command Bus configuration:

And delayed retry configuration:

Command Bus with Delayed Retry

For the common case of "retry with delay, then dead-letter on exhaustion" you don't need to wire an ErrorHandlerConfiguration separately — declare the policy inline on your gateway with #[DelayedRetry]:

Failures from any command sent through ResilientCommandBus are routed to a generated Error Channel, retried with the configured backoff and, on exhaustion, routed to dbal_dead_letter.

#[DelayedRetry] and #[ErrorChannel] are mutually exclusive on the same gateway interface — choose #[ErrorChannel] when the failure destination is a channel you've already configured (typically via ErrorHandlerConfiguration) and want to share across multiple gateways; choose #[DelayedRetry] when the policy is specific to that gateway and you don't need to share it.

Per-Handler Error Channel for Asynchronous Handlers

When multiple asynchronous handlers share the same transport channel (e.g. 'orders'), each handler can declare its own Error Channel via the asynchronousExecution parameter of #[Asynchronous]. Failures are routed per-handler, so different handlers on the same transport can have different error-handling policies.

Both handlers consume from the 'orders' async transport, but a failure in processPayment lands in paymentsErrors, and a failure in ship lands in shippingErrors. The two policies can be completely different — one queue could feed a Dead Letter, the other could resend with delayed retries.

Resolution Order

When resolving where a failed message should go, Ecotone applies the most specific configuration first:

  1. Per-handler #[ErrorChannel] or #[DelayedRetry] declared via #[Asynchronous(asynchronousExecution: [...])] — wins for that specific handler.

  2. Per-channel error channel set explicitly on the consumer's PollingMetadata (e.g. PollingMetadata::create('orders')->setErrorChannelName(...)).

  3. Global default error channel from withDefaultErrorChannel(...).

This means a per-handler attribute overrides the global default error channel for that handler only — other handlers on the same transport continue to use the default.

Per-Handler Delayed Retry for Asynchronous Handlers

Use #[ErrorChannel] when you want the handler to use a predefined error channel — typically one set up via an ErrorHandlerConfiguration extension object — so several handlers can share the same retry + dead-letter policy.

Use #[DelayedRetry] when the policy is specific to a single handler and doesn't need to be reused. The retry shape and dead letter destination are declared inline on the handler — failures are retried with the configured delay/backoff and, on exhaustion, routed to the dead letter channel you specify.

Parameters

Parameter
Required
Description

initialDelayMs

yes

Delay before the first retry, in milliseconds

multiplier

no (default 1)

Backoff multiplier between retries; 1 = constant delay, >1 = exponential

maxDelayMs

no

Cap on the delay between retries; null = no cap

maxAttempts

no (default 3)

Maximum number of retries before giving up; null = unlimited

deadLetterChannel

no

Where to route a Message after retries are exhausted; null = the failure throws and is handled by the consumer's FinalFailureStrategy

Mutual exclusion with #[ErrorChannel]

#[DelayedRetry] and #[ErrorChannel] cannot both appear on the same handler — they are alternative error-handling strategies:

  • #[ErrorChannel] — point failures at an error channel you've already defined elsewhere (typically via an ErrorHandlerConfiguration extension object, which sets up retry + dead-letter behaviour once and gives it a name). Multiple handlers can share the same configured channel, so the retry/dead-letter policy lives in one place and is reused across the application.

  • #[DelayedRetry] — declare the retry shape and dead letter destination directly on the handler; useful when the policy is specific to that handler and you don't want to share it.

If both attributes are declared on the same handler, the application fails to bootstrap with a descriptive ConfigurationException.

Last updated

Was this helpful?