Contents
- Default Error Handler
- onException()
- Redelivery & Backoff
- Dead Letter Channel
- doTry / doCatch / doFinally
- Circuit Breaker
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.