Skip to main content

ErrorProcessor

An ErrorProcessor reacts to a pipeline failure. When any Transformer or Processor throws an unhandled exception, the route's ErrorProcessor chain runs in declared order until one returns true (the failure is recovered, the source Connector receives an Ack) or all return false (the failure stands, the source receives a Nack).

PropertyValue
Java interfacecom.connamara.sdk.v1.adapter.components.ErrorProcessor
Methodboolean handleError(Message<?> failedMessage, Throwable cause, RouteContext routeContext)
Pipeline slotTriggered on any unhandled exception from a Transformer or Processor. Runs outside the normal message path.
Return contracttrue if the failure is handled (Ack to source), false to defer to the next ErrorProcessor in the chain. If every ErrorProcessor returns false, the source Connector receives a Nack.
Side effectsExpected. Common patterns are dead-lettering, recording a diagnostic event, scheduling a retry, or alerting.

Complete example

This ErrorProcessor sends the failed message to a dead-letter connector and acknowledges, swallowing the failure so the source Connector can move on.

package com.example.myplugin;

import com.connamara.sdk.v1.adapter.AdapterContext;
import com.connamara.sdk.v1.adapter.RouteContext;
import com.connamara.sdk.v1.adapter.components.ErrorProcessor;
import com.connamara.sdk.v1.common.component.ManifestResource;
import com.connamara.sdk.v1.common.message.DerivedMessage;
import com.connamara.sdk.v1.common.message.Message;

import java.util.Map;

@ManifestResource("""
{
"id": "dead-letter-error-processor",
"pluginId": "my-plugin",
"functionalType": "dead-letter-error-processor",
"displayName": "Dead Letter on Failure",
"category": "ERROR_PROCESSOR",
"configuration": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "Dead Letter Settings",
"x-order": ["deadLetterTarget"],
"required": ["deadLetterTarget"],
"properties": {
"deadLetterTarget": {
"type": "string",
"title": "Dead Letter Target",
"description": "The logical id of the connector that receives failed messages."
}
}
}
}
""")
public class DeadLetterErrorProcessor implements ErrorProcessor {

private final String deadLetterTarget;
private final AdapterContext adapterContext;

// handleError is handed only a RouteContext, which can reach the route's own
// source or target connection but not an arbitrary named connector. To send
// to a configurable dead-letter connector, the component holds the
// AdapterContext. The plugin's createComponent supplies it (see below).
public DeadLetterErrorProcessor(Map<String, Object> configuration, AdapterContext adapterContext) {
this.deadLetterTarget = (String) configuration.get("deadLetterTarget");
this.adapterContext = adapterContext;
}

@Override
public boolean handleError(Message<?> failedMessage, Throwable cause, RouteContext routeContext) {
Message<?> annotated = DerivedMessage.derive(
failedMessage,
failedMessage.getPayload(),
Map.of(
"x-error-class", cause.getClass().getName(),
"x-error-message", cause.getMessage() == null ? "" : cause.getMessage()
)
);
try {
adapterContext.sendToConnector(deadLetterTarget, annotated);
return true;
} catch (Exception e) {
// sendToConnector is a checked operation. If the dead-letter write
// fails, return false so the failure passes to the next
// ErrorProcessor (and the source Nacks if the chain runs out) rather
// than letting the exception escape handleError.
return false;
}
}
}

AdapterContext.sendToConnector(connectorId, message) routes to any connector on the adapter by its logical id, which is what a configurable dead-letter destination needs. The component receives the AdapterContext from its plugin entry point: the plugin stores the context handed to AdapterPlugin.setAdapterContext(...) and passes it into the constructor from createComponent, exactly as the entry point already supplies each component's configuration map (see Getting Started). If the destination is simply the route's own configured target connection, routeContext.sendToTarget(annotated) sends there directly and needs no AdapterContext; it declares the same checked exception, so wrap it the same way.


Key rules

  • Return true to recover. A true return tells the platform the failure is handled. The source Connector receives an Ack and moves on. Use this when the message has been routed to a dead-letter, scheduled for retry, or deliberately suppressed.
  • Return false to defer. A false return passes the failure to the next ErrorProcessor in the chain. If the chain runs out, the source receives a Nack. Use this for ErrorProcessors that only handle specific exception types.
  • Never throw. An exception from inside handleError is treated as a fatal failure: the route's error chain is exhausted and the source receives a Nack. The send calls (AdapterContext.sendToConnector, RouteContext.sendToTarget, RouteContext.sendToSource) all declare a checked exception, so wrap them in try/catch and return false rather than letting the exception escape.
  • Inspect cause to be selective. Most production ErrorProcessors check cause.getClass() (or cause instanceof MyException) and return false for cases they don't intend to handle, leaving them for downstream ErrorProcessors.

Test pattern

@Test
@DisplayName("GIVEN any failure WHEN handled THEN annotates the message and routes to the dead-letter connector")
void handleError_routesToDeadLetterAndReturnsTrue() throws Exception {
AdapterContext adapterContext = mock(AdapterContext.class);
DeadLetterErrorProcessor processor =
new DeadLetterErrorProcessor(Map.of("deadLetterTarget", "dlq"), adapterContext);

Message<?> message = mock(Message.class);
when(message.getPayload()).thenReturn("payload");
when(message.getHeaders()).thenReturn(new MessageHeaders(Map.of()));

boolean handled = processor.handleError(message, new RuntimeException("boom"), mock(RouteContext.class));

assertTrue(handled);
verify(adapterContext).sendToConnector(eq("dlq"), any(Message.class));
}

@Test
@DisplayName("GIVEN the dead-letter send fails WHEN handled THEN returns false and does not throw")
void handleError_sendFails_returnsFalse() throws Exception {
AdapterContext adapterContext = mock(AdapterContext.class);
doThrow(new RuntimeException("connector unavailable"))
.when(adapterContext).sendToConnector(anyString(), any(Message.class));
DeadLetterErrorProcessor processor =
new DeadLetterErrorProcessor(Map.of("deadLetterTarget", "dlq"), adapterContext);

Message<?> message = mock(Message.class);
when(message.getPayload()).thenReturn("payload");
when(message.getHeaders()).thenReturn(new MessageHeaders(Map.of()));

boolean handled = processor.handleError(message, new RuntimeException("boom"), mock(RouteContext.class));

assertFalse(handled);
}

When to reach for an ErrorProcessor

NeedReach for
Suppress a known transient failure so the source can Ack and move on.ErrorProcessor returning true.
Send failed messages to a dead-letter store for later inspection.ErrorProcessor returning true after a side-channel send.
Decide based on the cause whether to retry or give up.ErrorProcessor that inspects cause and either returns true (after scheduling a retry) or false (to let the source Nack).
Validate input upfront and drop bad messages cleanly.Processor returning null, not an ErrorProcessor. ErrorProcessors are for unexpected failures, not validation.

See also