Apache Camel: Error Handling & onException
Camel provides a rich error handling model: every route has a default error handler, and you can override it per route or per exception type using onException() clauses for fine-grained retry, redelivery, and dead letter channel logic.
Contents
Default Error Handler
By default every Camel route uses the DefaultErrorHandler
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");
}
}
errorHandler(noErrorHandler())onException()
Declare onException()RouteBuilder.configure()handled(true)
@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}");
}
}
handled(true)onExceptionhandled(true)handled(false)Redelivery & Backoff
Configure automatic redelivery with delay and exponential backoff inside onException()
@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");
}
}
Dead Letter Channel
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)
// send the ORIGINAL message to the DLC, not the modified one
.useOriginalMessage()
.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()doTry / doCatch / doFinally
doTryonException
@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/doCatchonException()maximumRedeliveries()Circuit Breaker
The circuit breaker pattern prevents cascading failures by stopping calls to a struggling service and allowing it time to recover. Add camel-resilience4j
<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