Skip to content

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:

  1. Specific catches — any clause with exceptions: and/or when: — are tried first in declaration order.
  2. 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") and ex.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-finally always 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 headers HelixFailureMessage (error string) and HelixFailureEndpoint (the fromUri of the route).
  • Clears ex.SetException(nil) so the DLQ pipeline runs (BuildPipeline short-circuits on IsFailed() 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.

steps:
  - validate: body().amount > 0
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

catch := processor.NewOnException(handler, processor.ErrorContains("timeout"))

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

catch := processor.NewOnException(handler, processor.ErrorIs(io.EOF))

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) plus errors.Is matchers in Go-defined routes.
  • do-finally errors are logged and dropped. Don't rely on finally to detect failures.
  • A DeadLetterChannel whose DLQ pipeline itself fails will surface a wrapping error; the original is preserved on HelixExceptionCaught.
  • OnException.Handled = false rethrows 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, Timeout for transient-failure handling.
  • eip_advanced — Rollback, Stop for explicit termination.
  • on_completion — success/failure callbacks.