Intercept and control error handling before the standard retry/DLQ chain
The @OnError annotation marks a method as a custom error handler inside a @ChangeStream class. When a handler method throws an exception, FlowWarden consults @OnError methods before the standard retry/DLQ chain, giving you full control over what happens next.
Any other signature will cause a BeanCreationException at startup. The return type must be ErrorAction — void is not supported. The parameter order is (Throwable, ChangeStreamContext), not the reverse.
The return value of an @OnError method tells FlowWarden what to do next:
Action
Behavior
SKIP
Ignore the event. Logs a warning, no retry, no DLQ. Stream continues.
RETRY
Force a retry. Respects @RetryPolicy.maxAttempts if present.
DLQ
Send directly to the Dead Letter Queue, bypassing any remaining retries.
RETHROW
Let FlowWarden handle with the standard policy (retry chain → DLQ).
RETRY and RETHROW both go through FlowWarden’s sequential retry engine — the stream does not advance to the next event during the retry sequence. See head-of-line blocking for the worked example and workarounds.
When multiple @OnError methods exist, FlowWarden picks the most specific match:
1. @OnError with exact type match2. @OnError with parent type match (closest in hierarchy)3. @OnError catch-all (empty value)4. Standard @RetryPolicy chain5. @DeadLetterQueue
@OnError methods always use the imperative signature — even in reactive streams. The reactive handler may return Mono.error(), but the error handler itself is synchronous and returns an ErrorAction directly.
If an @OnError method itself throws an exception, FlowWarden catches it, logs the error, and falls back to ErrorAction.RETHROW. This prevents error handlers from crashing the stream.
Use SKIP for programming errors (validation failures, malformed data) that retrying won’t fix.
Use RETRY for known transient errors where you want to bypass the exception type checks of @RetryPolicy.noRetryOn.
Use DLQ to short-circuit retries when you can determine that an error is permanent (e.g., external service returns HTTP 404).
Use RETHROW as a safe default in catch-all handlers — it lets the standard retry/DLQ chain do its job.
Keep error handlers simple. Avoid calling external services or performing database operations in @OnError methods — they should be fast decision-makers, not processors.
Use ctx.getAttemptNumber() in your error handler to make decisions based on the retry count. For example, return RETRY on first attempt but DLQ on subsequent attempts.