Contents

By default every Camel route uses the DefaultErrorHandler. When an exception escapes a processor, Camel logs it and propagates the exception back to the caller. No redelivery is attempted — the message is simply failed.

import org.apache.camel.builder.RouteBuilder; import org.springframework.stereotype.Component; @Component public class BasicErrorRoute extends RouteBuilder { @Override public void configure() { // The default error handler is active — exceptions are logged and re-thrown from("direct:start") .log("Processing: ${body}") .process(exchange -> { throw new RuntimeException("Something went wrong"); }) .log("This line is never reached"); } } To explicitly declare a no-op error handler (suppress all errors silently), use errorHandler(noErrorHandler()) at the top of a route. Use this with caution — swallowed exceptions make debugging very difficult.

Declare onException() at the start of a RouteBuilder.configure() to handle specific exception types for all routes in that builder. Use handled(true) to mark the exception as consumed and prevent it from propagating further.

@Component public class ExceptionHandlerRoute extends RouteBuilder { @Override public void configure() { // Handle IllegalArgumentException — log it and continue without rethrowing onException(IllegalArgumentException.class) .handled(true) // exception is consumed — exchange continues normally .log("Bad argument: ${exception.message}") .setBody(constant("ERROR: invalid input")); // Handle IOException — retry twice then send to error queue onException(java.io.IOException.class) .maximumRedeliveries(2) .redeliveryDelay(1000) .handled(true) .log("IO error after retries: ${exception.message}") .to("direct:errorQueue"); // Route that may throw from("direct:start") .process(exchange -> { String body = exchange.getMessage().getBody(String.class); if (body == null) { throw new IllegalArgumentException("Body must not be null"); } }) .log("Result: ${body}"); } } If handled(true) is not set, the exception is re-thrown after the onException block executes, which may cause the caller to receive an error. Always explicitly set handled(true) or handled(false) to document intent.

Configure automatic redelivery with delay and exponential backoff inside onException() or on the error handler directly. Camel re-routes the same exchange through the failed processor.

@Component public class RedeliveryRoute extends RouteBuilder { @Override public void configure() { onException(Exception.class) .maximumRedeliveries(4) // try up to 4 times after the first failure .redeliveryDelay(1000) // wait 1 second before first retry .backOffMultiplier(2.0) // double the delay each time: 1s, 2s, 4s, 8s .maximumRedeliveryDelay(30_000) // cap at 30 seconds .useExponentialBackOff() // enable exponential back-off .retryAttemptedLogLevel(org.apache.camel.LoggingLevel.WARN) // log retries at WARN .retriesExhaustedLogLevel(org.apache.camel.LoggingLevel.ERROR) .handled(true) .log("All retries exhausted for: ${body}") .to("direct:dlq"); from("direct:flaky") .log("Attempting to call external service") .to("http://unreliable.service/api"); } } Redelivery in Camel is synchronous — the consumer thread blocks during the delay. For long delays or high-volume consumers, use a dead letter channel and re-queue the message externally instead of blocking the thread.

The Dead Letter Channel (DLC) error handler sends failed messages to a designated dead letter endpoint after all redeliveries are exhausted. Configure it per-route using errorHandler(deadLetterChannel(...)).

import org.apache.camel.builder.RouteBuilder; import org.springframework.stereotype.Component; @Component public class DlcRoute extends RouteBuilder { @Override public void configure() { // Route-level dead letter channel — file based errorHandler( deadLetterChannel("file:data/errors?fileName=failed-${date:now:yyyyMMddHHmmss}.xml") .maximumRedeliveries(3) .redeliveryDelay(2000) .useOriginalMessage() // send the ORIGINAL message to the DLC, not the modified one .logExhausted(true) ); from("direct:processOrder") .log("Processing order: ${body}") .process(exchange -> { // Simulate an error throw new RuntimeException("Order processing failed"); }); } } // Kafka-backed dead letter channel errorHandler( deadLetterChannel("kafka:orders-dlq?brokers=localhost:9092") .maximumRedeliveries(5) .useOriginalMessage() ); useOriginalMessage() is important — without it, Camel sends whatever the message looks like at the point of failure (which may be partially transformed). With it, the original unmodified message is forwarded to the dead letter endpoint.

doTry provides an inline try-catch-finally block directly in the route DSL — useful when you want different error handling for one step within a larger route without defining a global onException.

@Component public class DoTryRoute extends RouteBuilder { @Override public void configure() { from("direct:start") .log("Starting route") .doTry() .process(exchange -> { // This step might fail String body = exchange.getMessage().getBody(String.class); if (body.contains("BAD")) { throw new IllegalArgumentException("Bad data: " + body); } exchange.getMessage().setBody("PROCESSED: " + body); }) .doCatch(IllegalArgumentException.class) .log("Caught IllegalArgumentException: ${exception.message}") .setBody(constant("INVALID_INPUT")) .doCatch(Exception.class) .log("Caught general exception: ${exception.message}") .setBody(constant("UNEXPECTED_ERROR")) .doFinally() .log("Always runs — cleanup here") .end() .log("Final body: ${body}"); } } doTry/doCatch does not perform automatic redelivery — it is a one-shot try/catch. For retry logic, use onException() with maximumRedeliveries() instead.

The circuit breaker pattern prevents cascading failures by stopping calls to a struggling service and allowing it time to recover. Add camel-resilience4j to use Resilience4j as the circuit breaker implementation.

<dependency> <groupId>org.apache.camel.springboot</groupId> <artifactId>camel-resilience4j-starter</artifactId> </dependency> import org.apache.camel.builder.RouteBuilder; import org.springframework.stereotype.Component; @Component public class CircuitBreakerRoute extends RouteBuilder { @Override public void configure() { from("direct:callExternalService") .circuitBreaker() // Attempt the call inside the circuit breaker .to("http://external-service/api/data") .log("Call succeeded: ${body}") .onFallback() // This runs when the circuit is OPEN or the call fails .setBody(constant("Fallback response — service is unavailable")) .log("Circuit breaker fallback triggered") .end(); } } # Resilience4j circuit breaker configuration camel.resilience4j.failure-rate-threshold=50 camel.resilience4j.wait-duration-in-open-state=10000 camel.resilience4j.sliding-window-size=10 The circuit breaker has three states: CLOSED (normal operation), OPEN (calls are short-circuited to the fallback), and HALF-OPEN (test calls are allowed through to check recovery). The failure rate threshold controls when the circuit opens.