Error handling processors¶
pkg/processor/error_handling.go defines Helix's error-handling primitives: try/catch/finally (DoTry), dead-letter routing with redelivery (DeadLetterChannel), typed exception handlers (OnException), and predicate-based validation (Validate). Together they cover the Dead Letter Channel and exception-handling patterns from the EIP catalog plus the on-error-continue vs on-error-propagate semantics.
Processors in this file¶
NewDoTry¶
A try/catch/finally block around a pipeline.
YAML form:
steps:
- do-try:
steps:
- target: http://flaky-service
do-catch:
- exceptions: ["TimeoutError", "ConnectionRefused"]
when: header("X-Retry-Eligible") == "true"
on-error-continue: true
steps:
- log: { message: "swallowed", level: WARN }
- steps: # catch-all
- target: kafka://dlq
do-finally:
- log: { message: "always runs" }
Top-level fields:
| Field | Type | Default | Description |
|---|---|---|---|
steps |
list | required | The try block. |
do-catch |
list of catch clauses | optional | Catches; evaluated in two passes (see below). |
do-finally |
list | optional | Pipeline that runs whether or not the try succeeded. |
Catch clause fields:
| Field | Type | Default | Description |
|---|---|---|---|
exceptions |
list of strings | empty | Substrings tested via ErrorContains. ANY match selects the catch. Combined with when: using AND. |
when |
string | optional | Starlang predicate evaluated against the live exchange + error. Combined with exceptions: using AND. |
on-error-continue |
bool | true |
When true (Mule's on-error-continue): swallow the error, run the catch, route continues normally. When false (on-error-propagate): catch runs for cleanup but the original error is rethrown. |
steps |
list | required | Handler pipeline. The original error is exposed on property("HelixExceptionCaught"). |
Two-pass matching:
- Specific catches — any clause with
exceptions:and/orwhen:— are tried first in declaration order. - Catch-alls — clauses with neither — run last.
This mirrors Mule's "specific handlers win, then the catch-all".
Behavior on error:
- The error is exposed as
property("HelixExceptionCaught")andex.SetException(nil)is called so the catch pipeline runs cleanly. on-error-continue: true: catch runs, exchange continues. A failing catch surfaces its own error (broken handler shouldn't be masked).on-error-continue: false: catch runs (for cleanup), then the original error is restored on the exchange and returned. If the catch's own pipeline failed too, that error is returned and the original is preserved in the wrapping message.do-finallyalways runs after the try (and any catch), with its own errors logged but not surfaced.
Constructors: NewDoTry(tryProc), then chain .AddCatch(proc, matchers...) for legacy continue-true semantics or .AddCatchFull(DoCatch{...}) for full control. .SetFinally(proc).
NewDeadLetterChannel¶
The route-level error handler. Catches errors raised anywhere in the route, routes the failing exchange to a DLQ endpoint, and supports redelivery with exponential backoff.
error-handler:
dead-letter-channel: kafka://dlq
max-redeliveries: 5
redelivery-delay: 1s
backoff-multiplier: 2.0
| Field | Type | Default | Description |
|---|---|---|---|
dead-letter-channel |
string URI | required | Where to send the failed exchange after redeliveries are exhausted. |
max-redeliveries |
int | 0 |
How many times the engine retries the failing step BEFORE handing to the DLQ. 0 means no retry — DLQ on first failure. |
redelivery-delay |
duration | 0 |
Delay between retries. |
backoff-multiplier |
float | 0 |
Multiplier applied to the delay each retry. |
OnPrepareFailure (Go-only) |
Processor | nil | Optional processor invoked on the exchange just before DLQ delivery (e.g. to redact sensitive fields). |
Constructor: NewDeadLetterChannel(dlqProcessor engine.Processor, maxRedeliveries int) *DeadLetterChannel. Implements engine.RedeliveryAware, so engine.BuildPipeline reads (max, delay, backoff) from RedeliveryConfig() and performs the retry loop.
On final failure:
- Sets
HelixFailureRouteId(workflow ID),HelixExceptionCaught(the error), and the headersHelixFailureMessage(error string) andHelixFailureEndpoint(thefromUriof the route). - Clears
ex.SetException(nil)so the DLQ pipeline runs (BuildPipelineshort-circuits onIsFailed()otherwise). - If the DLQ pipeline itself errors, the original exception is restored on the exchange so the caller sees a real failure.
NewOnException¶
A simpler error-handler variant for typed handling without redelivery semantics. Matches specific exception types via a list of func(error) bool matchers and either swallows or rethrows.
error-handlers:
- id: handle-validation
on-error:
- exceptions: ["ValidationError"]
steps:
- log: { message: "bad input" }
Constructor: NewOnException(processor engine.Processor, matchers ...func(error) bool) *OnException. The Handled field (default true) controls swallow-vs-rethrow.
NewValidate¶
Asserts a predicate over the exchange; returns a *ValidationError if it fails. Pairs naturally with do-try matching on ValidationError.
| Field | Type | Default | Description |
|---|---|---|---|
| (string body) | string | required | Starlang predicate. |
For richer messages use the Go constructor NewValidate(predicate engine.Predicate, message string) *Validate. The error type is *ValidationError, so catches can target it specifically (see ErrorContains("validation")).
Error matchers¶
Helpers exported for building catch clauses in Go:
ErrorContains¶
Returns a func(error) bool that does a substring match against err.Error(). This is what the YAML reifier uses to turn exceptions: ["X"] into matchers.
ErrorIs¶
Wraps errors.Is for matching wrapped errors against sentinel values.
Pitfalls¶
- The matching is substring, not type-checked. Choose error message wording carefully if you depend on it for catch routing — the safer pattern is to use
*ValidationError(typed) pluserrors.Ismatchers in Go-defined routes. do-finallyerrors are logged and dropped. Don't rely onfinallyto detect failures.- A
DeadLetterChannelwhose DLQ pipeline itself fails will surface a wrapping error; the original is preserved onHelixExceptionCaught. OnException.Handled = falserethrows after the handler runs — useful for tracing/notification handlers that shouldn't claim ownership of the failure.
Examples¶
Try/catch with continue semantics:
workflows:
- id: tolerant-publish
trigger: { uri: direct://orders }
steps:
- do-try:
steps:
- target: kafka://orders
do-catch:
- exceptions: ["timeout"]
on-error-continue: true
steps:
- log: { message: "kafka timeout, falling back", level: WARN }
- target: postgres://order-fallback
- steps:
- target: kafka://dlq
do-finally:
- log: { message: "publish attempt complete" }
Route-level DLQ with redelivery:
error-handlers:
- id: default-dlq
on-error:
- steps:
- target: kafka://dlq
workflows:
- id: orders
trigger: { uri: kafka://orders-in }
error-handler-ref: default-dlq
steps:
- target: postgres://orders
Source¶
pkg/processor/error_handling.go
See also¶
- resilience —
Retry,CircuitBreaker,Timeoutfor transient-failure handling. - eip_advanced —
Rollback,Stopfor explicit termination. - on_completion — success/failure callbacks.