Extremely Serious

Month: February 2026

Apache Camel Error Handling: DefaultErrorHandler and onException

Apache Camel's robust error handling ensures reliable integration routes even when failures occur. The DefaultErrorHandler provides automatic retries and logging, while onException clauses offer precise control over specific exceptions.

Core Concepts

DefaultErrorHandler is Camel’s default strategy for all routes, featuring retry logic, detailed logging, and flexible recovery paths. It logs exceptions at ERROR level by default and supports configurable redeliveries without requiring dead letter channels.

onException clauses intercept specific exception types globally or per-route, with priority based on exception specificity—most-derived classes match first. Use handled(true) to stop propagation and transform responses, or continued(true) to resume the route.

DefaultErrorHandler Configuration

Set global retry policies early in RouteBuilder’s configure() method:

errorHandler(defaultErrorHandler()
    .maximumRedeliveries(3)
    .redeliveryDelay(2000)
    .retryAttemptedLogLevel(LoggingLevel.WARN)
    .logStackTrace(true));

Key options control retry behavior: maximumRedeliveries caps attempts, redeliveryDelay sets wait time (with exponential backoff), and retryAttemptedLogLevel adjusts retry logging verbosity. Per-route overrides nest within from() endpoints for granular control.

onException Best Practices

Place onException handlers before route definitions for global scope. Multiple handlers for the same exception type chain by priority, with conditions via onWhen():

onException(IOException.class)
    .handled(true)
    .log("🚨 IO EXCEPTION: ${exception.message}")
    .to("direct:ioErrorHandler");

onException(IllegalArgumentException.class)
    .onWhen(exchange -> "high".equals(exchange.getIn().getHeader("error.priority")))
    .handled(true)
    .log("⚠️ HIGH PRIORITY: ${exception.message}");

The handled(true) flag clears the exception and prevents DefaultErrorHandler retries unless continued(true) is specified. Route to dedicated error flows like direct:ioErrorHandler for consistent processing.

Example

This self-contained demo showcases layered error handling across exception types, conditional logic, and retry policies:

import org.apache.camel.*;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.impl.DefaultCamelContext;

public class CamelErrorHandlingDemo extends RouteBuilder {

    @Override
    public void configure() throws Exception {
        // Global DefaultErrorHandler with retries
        errorHandler(defaultErrorHandler()
            .maximumRedeliveries(3)
            .redeliveryDelay(2000)
            .retryAttemptedLogLevel(LoggingLevel.WARN)
            .logStackTrace(true));

        // Specific exception handlers (priority: most specific first)
        onException(java.io.IOException.class)
            .handled(true)
            .log("🚨 IO EXCEPTION HANDLED FIRST: ${exception.message}")
            .to("direct:ioErrorHandler");

        onException(IllegalArgumentException.class)
            .maximumRedeliveries(2)
            .redeliveryDelay(1000)
            .onWhen(exchange -> "high".equals(exchange.getIn().getHeader("error.priority")))
            .handled(true)
            .log("⚠️ HIGH PRIORITY ARG EXCEPTION: ${exception.message}")
            .to("direct:argErrorHandler");

        onException(IllegalArgumentException.class)
            .handled(true)
            .log("⚠️ DEFAULT ARG EXCEPTION: ${exception.message}")
            .to("direct:argErrorHandler");

        onException(Exception.class)
            .handled(true)
            .log("💀 GENERIC EXCEPTION: ${exception.message}")
            .to("direct:deadLetterQueue");

        // Main processing route
        from("direct:start")
            .routeId("mainRoute")
            .log("Processing: ${body}")
            .process(mainProcessor())
            .to("direct:success");

        from("direct:success").log("✅ SUCCESS: ${body}");
        from("direct:ioErrorHandler").log("📁 IO Error handled").setBody(constant("IO_PROCESSED"));
        from("direct:argErrorHandler").log("🔄 Arg error processed").setBody(constant("ARG_PROCESSED"));
        from("direct:deadLetterQueue").log("⚰️ DLQ: ${exception.stacktrace}").setBody(constant("DLQ_PROCESSED"));
    }

    private static Processor mainProcessor() {
        return exchange -> {
            String input = exchange.getIn().getBody(String.class);
            switch (input != null ? input : "") {
                case "io-fail":
                    exchange.getIn().setHeader("error.priority", "high");
                    throw new java.io.IOException("💾 Disk full");
                case "arg-high":
                    exchange.getIn().setHeader("error.priority", "high");
                    throw new IllegalArgumentException("❌ High priority invalid data");
                case "arg-normal":
                    throw new IllegalArgumentException("❌ Normal invalid data");
                case "generic-fail":
                    throw new RuntimeException("Unknown error");
                default:
                    exchange.getIn().setBody("✅ Processed: " + input);
            }
        };
    }

    public static void main(String[] args) throws Exception {
        try (CamelContext context = new DefaultCamelContext()) {
            context.getCamelContextExtension().setName("CamelErrorDemo");
            context.addRoutes(new CamelErrorHandlingDemo());
            context.start();

            ProducerTemplate template = context.createProducerTemplate();
            System.out.println("🚀 Camel Error Handling Demo...\n");

            template.sendBody("direct:start", "io-fail");      // → IO handler
            template.sendBody("direct:start", "arg-high");     // → High priority arg
            template.sendBody("direct:start", "arg-normal");   // → Default arg  
            template.sendBody("direct:start", "generic-fail"); // → Generic handler
            template.sendBody("direct:start", "good");         // → Success

            Thread.sleep(8000);
        }
    }
}

Expected Demo Output

textProcessing: io-fail
🚨 IO EXCEPTION HANDLED FIRST: 💾 Disk full
📁 IO Error handled

Processing: arg-high  
⚠️ HIGH PRIORITY ARG EXCEPTION: ❌ High priority invalid data
🔄 Arg error processed

Production Tips

Test all failure paths during development using predictable payloads like the demo. Monitor exchange.getProperty(Exchange.REDELIVERY_COUNTER) for retry stats. For complex flows, combine with doTry().doCatch() blocks for local exception handling within routes. This approach scales from simple REST APIs to enterprise integration patterns.

Dead Letter Channel in Apache Camel with Log Endpoint

Apache Camel's Dead Letter Channel (DLC) is an Enterprise Integration Pattern that gracefully handles failed messages by routing them to a designated endpoint, preventing route blockages. This article explores DLC configuration using a lightweight log endpoint—no JMS broker required—ideal for development, testing, or simple logging setups. You'll get a complete, runnable example to see it in action.

What is Dead Letter Channel?

The DLC activates when a message processing fails after exhausting retries. Unlike Camel's default error handler, which logs exceptions, DLC preserves the original input message and forwards it to a "dead letter" endpoint like a log, file, or queue. Key benefits include message isolation for later analysis and non-blocking route behavior.

It supports redelivery policies for automatic retries before final routing. This pattern shines in enterprise integration where failures (e.g., network issues, data validation errors) must not halt overall flow.

Why Use Log Endpoint?

The log component offers zero-setup error handling: it captures failures at ERROR level with full exchange details (body, headers, exception stack trace). Perfect for:

  • Quick debugging without external dependencies.
  • Console output in standalone apps or containers.
  • Custom formatting via options like showAll=true or multiline=true.

No database, file system, or broker needed—run it anywhere Camel supports logging (SLF4J/Logback by default).

Complete Working Example

This standalone Java app uses Camel Main to simulate failures via a timer-triggered route. It retries twice, then logs the failure.

Java DSL Code

import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.main.Main;

public class DeadLetterLogDemo extends RouteBuilder {
    @Override
    public void configure() {
        // Configure DLC with log endpoint
        errorHandler(deadLetterChannel("log:dead?level=ERROR&showAll=true&multiline=true")
            .useOriginalMessage()  // Preserves original input
            .maximumRedeliveries(2)
            .redeliveryDelay(1000)
            .retryAttemptedLogLevel(org.apache.camel.LoggingLevel.WARN));

        // Route that simulates failure
        from("timer:fail?period=5000&repeatCount=1")
            .log("🔥 Starting processing: ${body}")
            .process(exchange -> {
                // Simulate a runtime failure (e.g., bad data parsing)
                throw new RuntimeException("💥 Simulated processing error!");
            })
            .log("✅ Success log (won't reach here)");
    }

    public static void main(String[] args) throws Exception {
        Main main = new Main();
        main.configure().addRoutesBuilder(new DeadLetterLogDemo());
        main.run(args);  // Runs until Ctrl+C
    }
}

Expected Console Output

🔥 Starting processing:
Failed delivery for (MessageId: 25B7DFF9FB26DD8-0000000000000000 on ExchangeId: 25B7DFF9FB26DD8-0000000000000000). On delivery attempt: 0 caught: java.lang.RuntimeException: 💥 Simulated processing error!
Failed delivery for (MessageId: 25B7DFF9FB26DD8-0000000000000000 on ExchangeId: 25B7DFF9FB26DD8-0000000000000000). On delivery attempt: 1 caught: java.lang.RuntimeException: 💥 Simulated processing error!
Failed delivery for (MessageId: 25B7DFF9FB26DD8-0000000000000000 on ExchangeId: 25B7DFF9FB26DD8-0000000000000000). On delivery attempt: 2 caught: java.lang.RuntimeException: 💥 Simulated processing error!
Exchange[
  Id: 25B7DFF9FB26DD8-0000000000000000
  RouteGroup: null
  RouteId: route1
  ExchangePattern: InOnly
  Properties: {CamelExceptionCaught=java.lang.RuntimeException: 💥 Simulated processing error!, CamelFailureRouteId=route1, CamelFatalFallbackErrorHandler=[route1], CamelToEndpoint=log://dead?level=ERROR&multiline=true&showAll=true}

Advanced Customizations

Enhance with processors for richer logging:

errorHandler(deadLetterChannel("log:dead?level=ERROR")
    .onExceptionOccurred(exchange -> {
        exchange.getIn().setHeader("failureTime", System.currentTimeMillis());
        exchange.getIn().setHeader("errorDetails", exchange.getException().getMessage());
    }));

Best Practices

  • Always pair with useOriginalMessage() to avoid losing input data.
  • Set maximumRedeliveries=0 for immediate DLC without retries in tests.
  • Monitor log volume in production; consider rotating to file or alerting.
  • Test edge cases: network timeouts, serialization errors, poison messages.
  • Global DLC via camelContext.setErrorHandlerFactory() for app-wide coverage.

This setup provides robust, observable error handling. Experiment by changing the failure to a real processor (e.g., invalid JSON parsing) for your use cases.

Apache Camel Transformer

Apache Camel's Transformer EIP enables declarative, type-safe message conversion between POJOs in routes. Below is a fully self-contained, single-file example using nested classes and Camel's Main class.

Complete Single-File Example

import org.apache.camel.Message;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.main.Main;
import org.apache.camel.spi.DataType;
import org.apache.camel.spi.Transformer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Apache Camel Transformer EIP example demonstrating declarative type-safe message conversion.
 * All classes (Lead, Customer, LeadToCustomerTransformer) are in this single file.
 */
public class TransformerDemo {
    private static final Logger LOG = LoggerFactory.getLogger(TransformerDemo.class);

    /**
     * Lead POJO - Input model
     */
    public static class Lead {
        private String name;
        private String company;
        private String city;

        public Lead() {}

        public Lead(String name, String company, String city) {
            this.name = name;
            this.company = company;
            this.city = city;
        }

        // Getters and setters
        public String getName() { return name; }
        public void setName(String name) { this.name = name; }
        public String getCompany() { return company; }
        public void setCompany(String company) { this.company = company; }
        public String getCity() { return city; }
        public void setCity(String city) { this.city = city; }

        @Override
        public String toString() {
            return "Lead{name='" + name + "', company='" + company + "', city='" + city + "'}";
        }
    }

    /**
     * Customer POJO - Output model
     */
    public static class Customer {
        private String fullName;
        private String organization;
        private String location;

        public Customer() {}

        public Customer(String fullName, String organization, String location) {
            this.fullName = fullName;
            this.organization = organization;
            this.location = location;
        }

        // Getters and setters
        public String getFullName() { return fullName; }
        public void setFullName(String fullName) { this.fullName = fullName; }
        public String getOrganization() { return organization; }
        public void setOrganization(String organization) { this.organization = organization; }
        public String getLocation() { return location; }
        public void setLocation(String location) { this.location = location; }

        @Override
        public String toString() {
            return "Customer{fullName='" + fullName + "', organization='" + organization + "', location='" + location + "'}";
        }
    }

    /**
     * Transformer implementation for converting Lead to Customer
     */
    public static class LeadToCustomerTransformer extends Transformer {
        @Override
        public void transform(Message message, DataType fromType, DataType toType) throws Exception {
            Lead lead = message.getMandatoryBody(Lead.class);
            Customer customer = new Customer(
                lead.getName().toUpperCase(),
                lead.getCompany(),
                lead.getCity()
            );
            message.setBody(customer);
        }
    }

    public static void main(String[] args) throws Exception {
        Main main = new Main();
        main.configure().addRoutesBuilder(new RouteBuilder() {
            @Override
            public void configure() {
                // Register the transformer
                transformer()
                    .fromType("java:TransformerDemo$Lead")
                    .toType("java:TransformerDemo$Customer")
                    .withJava(LeadToCustomerTransformer.class);

                // Main route with automatic transformation
                from("direct:start")
                    .inputType("java:TransformerDemo$Lead")
                    .log("INPUT Lead: ${body}")
                    .to("direct:process")  // Triggers transformer automatically
                    .log("OUTPUT Customer: ${body}");

                from("direct:process")
                    .inputType("java:TransformerDemo$Customer")
                    .log("PROCESSED Customer: ${body}")
                    .to("log:final?showAll=true");
            }
        });

        // Add a startup listener to send test message after Camel starts
        main.addMainListener(new org.apache.camel.main.MainListenerSupport() {
            @Override
            public void afterStart(org.apache.camel.main.BaseMainSupport mainSupport) {
                try {
                    LOG.info("Sending test Lead message...");
                    mainSupport.getCamelContext().createProducerTemplate()
                        .sendBody("direct:start", new Lead("John Doe", "Acme Corp", "Auckland"));
                    LOG.info("Test message sent successfully!");
                } catch (Exception e) {
                    LOG.error("Failed to send test message", e);
                }
            }
        });

        // Run Camel (will run until Ctrl+C is pressed)
        main.run(args);
    }
}

Expected Console Output

Sending test Lead message...
INPUT Lead: Lead{name='John Doe', company='Acme Corp', city='Auckland'}
PROCESSED Customer: Customer{fullName='JOHN DOE', organization='Acme Corp', location='Auckland'}
Exchange[Id: D0B1A9A95984A67-0000000000000000, RouteGroup: null, RouteId: route2, ExchangePattern: InOnly, Properties: {CamelToEndpoint=log://final?showAll=true}, Headers: {}, BodyType: TransformerDemo.Customer, Body: Customer{fullName='JOHN DOE', organization='Acme Corp', location='Auckland'}]
OUTPUT Customer: Customer{fullName='JOHN DOE', organization='Acme Corp', location='Auckland'}
Test message sent successfully!

Key Implementation Details

Nested Class References: Uses TransformerDemo$Lead syntax to reference nested classes in data type strings.

Transformer Method Signature: Uses transform(Message message, DataType fromType, DataType toType)—the modern Camel 4.x+ API.

Automatic Startup Test: MainListenerSupport.afterStart() sends test message automatically when Camel context starts.

Single-File Design: All POJOs, transformer, and routes contained in one compilable .java file.

How the Transformation Triggers

  1. Input validationinputType("java:TransformerDemo$Lead") expects Lead
  2. Route boundaryto("direct:process") requires Customer input type
  3. Type mismatch → Camel matches Lead→Customer transformer automatically
  4. Conversion executesLeadToCustomerTransformer.transform() runs
  5. Type contract satisfied → Route continues with Customer