CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-dev-langchain4j--langchain4j-agentic

LangChain4j Agentic Framework provides a comprehensive Java library for building multi-agent AI systems with support for workflow orchestration, supervisor agents, planning-based execution, declarative configuration, agent-to-agent communication, and human-in-the-loop workflows.

Overview
Eval results
Files

error-handling.mddocs/advanced/

Error Handling

Comprehensive error handling patterns for workflows with retry, recovery, and fallback strategies.

Overview

Error handling enables:

  • Retry logic with customizable strategies
  • Fallback mechanisms
  • Error context access
  • Recovery decisions (retry, return result, throw)
  • Per-agent and workflow-level handlers

ErrorHandler Configuration

<S> S errorHandler(Function<ErrorContext, ErrorRecoveryResult> errorHandler);

Available on all workflow builders:

  • agentBuilder()
  • sequenceBuilder()
  • parallelBuilder()
  • loopBuilder()
  • conditionalBuilder()
  • supervisorBuilder()
  • plannerBuilder()

ErrorContext

record ErrorContext(
    String agentName,
    AgenticScope agenticScope,
    AgentInvocationException exception
) {}

Access error details:

.errorHandler(ctx -> {
    String failedAgent = ctx.agentName();
    AgenticScope scope = ctx.agenticScope();
    AgentInvocationException error = ctx.exception();

    System.err.println("Agent: " + failedAgent);
    System.err.println("Error: " + error.getMessage());
    System.err.println("State: " + scope.state());

    return ErrorRecoveryResult.retry();
})

ErrorRecoveryResult

record ErrorRecoveryResult(Type type, Object result) {
    enum Type {
        THROW_EXCEPTION,  // Re-throw the exception
        RETURN_RESULT,    // Return result and continue
        RETRY             // Retry the failed operation
    }

    static ErrorRecoveryResult throwException();
    static ErrorRecoveryResult retry();
    static ErrorRecoveryResult result(Object result);
}

Recovery Strategies

Retry on Failure

UntypedAgent retryAgent = AgenticServices.sequenceBuilder()
    .subAgents(apiCaller)
    .errorHandler(ctx -> {
        AgenticScope scope = ctx.agenticScope();
        int retryCount = scope.readState("retry_count", 0);

        if (retryCount < 3) {
            scope.writeState("retry_count", retryCount + 1);
            return ErrorRecoveryResult.retry();
        }

        return ErrorRecoveryResult.throwException();
    })
    .build();

Return Fallback Result

UntypedAgent fallbackAgent = AgenticServices.conditionalBuilder()
    .subAgents(primaryAgent)
    .errorHandler(ctx -> {
        System.err.println("Primary failed, using fallback");
        return ErrorRecoveryResult.result("fallback-value");
    })
    .build();

Selective Recovery

UntypedAgent smartAgent = AgenticServices.loopBuilder()
    .subAgents(processor)
    .maxIterations(5)
    .errorHandler(ctx -> {
        Exception error = ctx.exception();

        if (error instanceof ValidationException) {
            // Fix and retry
            ctx.agenticScope().writeState("fixed", true);
            return ErrorRecoveryResult.retry();
        } else if (error instanceof RateLimitException) {
            // Wait and retry
            Thread.sleep(1000);
            return ErrorRecoveryResult.retry();
        } else {
            // Give up
            return ErrorRecoveryResult.result(
                Map.of("error", error.getMessage())
            );
        }
    })
    .build();

Common Patterns

Exponential Backoff

UntypedAgent backoffAgent = AgenticServices.agentBuilder()
    .chatModel(chatModel)
    .errorHandler(ctx -> {
        AgenticScope scope = ctx.agenticScope();
        int attempt = scope.readState("attempt", 0);

        if (attempt < 5) {
            scope.writeState("attempt", attempt + 1);

            // Exponential backoff
            long backoffMs = (long) Math.pow(2, attempt) * 1000;
            Thread.sleep(backoffMs);

            return ErrorRecoveryResult.retry();
        }

        return ErrorRecoveryResult.throwException();
    })
    .build();

Circuit Breaker

class CircuitBreaker implements Function<ErrorContext, ErrorRecoveryResult> {
    private int failureCount = 0;
    private boolean circuitOpen = false;
    private static final int THRESHOLD = 5;

    @Override
    public ErrorRecoveryResult apply(ErrorContext ctx) {
        if (circuitOpen) {
            return ErrorRecoveryResult.result("Circuit breaker open");
        }

        failureCount++;

        if (failureCount >= THRESHOLD) {
            circuitOpen = true;
            System.err.println("Circuit breaker opened");
        }

        return ErrorRecoveryResult.throwException();
    }

    public void reset() {
        failureCount = 0;
        circuitOpen = false;
    }
}

UntypedAgent protectedAgent = AgenticServices.sequenceBuilder()
    .subAgents(riskyAgent)
    .errorHandler(new CircuitBreaker())
    .build();

Error Classification

.errorHandler(ctx -> {
    Throwable error = ctx.exception();

    // Transient errors - retry
    if (error instanceof TimeoutException ||
        error instanceof IOException ||
        error instanceof ServiceUnavailableException) {

        int retries = ctx.agenticScope().readState("retries", 0);
        if (retries < 3) {
            ctx.agenticScope().writeState("retries", retries + 1);
            return ErrorRecoveryResult.retry();
        }
    }

    // Business errors - return result with error info
    if (error instanceof ValidationException ||
        error instanceof BusinessRuleException) {

        return ErrorRecoveryResult.result(
            Map.of("status", "error", "message", error.getMessage())
        );
    }

    // Fatal errors - propagate
    return ErrorRecoveryResult.throwException();
})

Error Aggregation

UntypedAgent parallelAgent = AgenticServices.parallelBuilder()
    .subAgents(agent1, agent2, agent3)
    .beforeCall(scope -> {
        scope.writeState("errors", new ArrayList<String>());
    })
    .errorHandler(ctx -> {
        List<String> errors = (List<String>) ctx.agenticScope().readState("errors");
        errors.add(ctx.agentName() + ": " + ctx.exception().getMessage());

        // Continue despite errors
        return ErrorRecoveryResult.result(null);
    })
    .output(scope -> {
        List<String> errors = (List<String>) scope.readState("errors");

        return Map.of(
            "results", scope.state(),
            "errors", errors,
            "success", errors.isEmpty()
        );
    })
    .build();

Compensating Actions

.errorHandler(ctx -> {
    AgenticScope scope = ctx.agenticScope();

    // Record what succeeded before failure
    List<String> completed = (List<String>) scope.readState("completed_steps");

    // Execute compensating actions in reverse order
    for (int i = completed.size() - 1; i >= 0; i--) {
        String step = completed.get(i);
        rollback(step);
    }

    // Return partial success info
    return ErrorRecoveryResult.result(
        Map.of(
            "status", "rolled_back",
            "completed", completed,
            "failed_at", ctx.agentName()
        )
    );
})

Loop-Specific Error Handling

UntypedAgent loop = AgenticServices.loopBuilder()
    .subAgents(processor)
    .maxIterations(10)
    .loopCounterName("attempt")
    .errorHandler(ctx -> {
        AgenticScope scope = ctx.agenticScope();
        Integer attempt = (Integer) scope.readState("attempt");

        // Log error
        System.err.println("Attempt " + attempt + " failed");

        // Track error history
        List<String> errorHistory = scope.readState("error_history", new ArrayList<>());
        errorHistory.add(ctx.exception().getMessage());
        scope.writeState("error_history", errorHistory);

        // Continue loop (retry)
        return ErrorRecoveryResult.retry();
    })
    .exitCondition(scope -> {
        // Exit if too many consecutive errors
        List<String> errors = scope.readState("error_history", new ArrayList<>());
        return errors.size() >= 5;
    })
    .build();

Declarative Error Handling

Example:

interface RobustSystem {
    @SequenceAgent(
        name = "robust-pipeline",
        subAgents = {Stage1.class, Stage2.class, Stage3.class}
    )
    String process(String input);

    @ErrorHandler
    ErrorRecoveryResult handleError(Throwable error, AgenticScope scope) {
        // Track error count
        int errorCount = scope.readState("error_count", 0);
        scope.writeState("error_count", errorCount + 1);

        // Retry transient errors
        if (error instanceof TransientException && errorCount < 3) {
            return ErrorRecoveryResult.retry();
        }

        // Return partial results for business errors
        if (error instanceof BusinessException) {
            return ErrorRecoveryResult.result(
                scope.readState("last_successful_result")
            );
        }

        // Propagate unexpected errors
        return ErrorRecoveryResult.throwException();
    }
}

Error Context Logging

.errorHandler(ctx -> {
    // Comprehensive error logging
    System.err.println("=== Error Report ===");
    System.err.println("Agent: " + ctx.agentName());
    System.err.println("Error Type: " + ctx.exception().getClass().getName());
    System.err.println("Message: " + ctx.exception().getMessage());
    System.err.println("Current State:");

    ctx.agenticScope().state().forEach((key, value) -> {
        System.err.println("  " + key + ": " + value);
    });

    System.err.println("Invocation History:");
    ctx.agenticScope().agentInvocations().forEach(invocation -> {
        System.err.println("  " + invocation.agentName() +
                         " -> " + invocation.result());
    });

    System.err.println("====================");

    return ErrorRecoveryResult.throwException();
})

Timeout Handling

UntypedAgent timeoutAgent = AgenticServices.agentBuilder()
    .chatModel(chatModel)
    .errorHandler(ctx -> {
        if (ctx.exception() instanceof TimeoutException) {
            AgenticScope scope = ctx.agenticScope();
            int timeoutCount = scope.readState("timeout_count", 0);

            if (timeoutCount < 2) {
                scope.writeState("timeout_count", timeoutCount + 1);
                // Increase timeout and retry
                return ErrorRecoveryResult.retry();
            } else {
                // Switch to fallback
                return ErrorRecoveryResult.result("timeout_fallback");
            }
        }

        return ErrorRecoveryResult.throwException();
    })
    .build();

Resource Cleanup

.errorHandler(ctx -> {
    AgenticScope scope = ctx.agenticScope();

    try {
        // Cleanup resources
        Connection conn = (Connection) scope.readState("db_connection");
        if (conn != null && !conn.isClosed()) {
            conn.rollback();
            conn.close();
        }

        FileHandle file = (FileHandle) scope.readState("temp_file");
        if (file != null) {
            file.delete();
        }

    } catch (Exception cleanupError) {
        System.err.println("Cleanup failed: " + cleanupError.getMessage());
    }

    return ErrorRecoveryResult.throwException();
})

Error Metrics

class MetricTrackingErrorHandler implements Function<ErrorContext, ErrorRecoveryResult> {
    private final MetricsCollector metrics;

    @Override
    public ErrorRecoveryResult apply(ErrorContext ctx) {
        // Record error metric
        metrics.recordError(
            ctx.agentName(),
            ctx.exception().getClass().getSimpleName()
        );

        // Track error rate
        double errorRate = metrics.getErrorRate(ctx.agentName());

        if (errorRate > 0.5) {
            // High error rate - circuit break
            return ErrorRecoveryResult.result("Service degraded");
        }

        // Retry with backoff
        int attempt = ctx.agenticScope().readState("attempt", 0);
        if (attempt < 3) {
            ctx.agenticScope().writeState("attempt", attempt + 1);
            return ErrorRecoveryResult.retry();
        }

        return ErrorRecoveryResult.throwException();
    }
}

Best Practices

  1. Always log errors - Include context for debugging
  2. Set retry limits - Prevent infinite loops
  3. Use exponential backoff - For transient failures
  4. Clean up resources - In error handlers
  5. Document error scenarios - In agent descriptions
  6. Track error metrics - For monitoring
  7. Test error paths - Don't just test happy paths
  8. Provide fallbacks - For critical operations

See Also

  • Loop Workflows - Loop error handling
  • Supervisor Agents - Supervisor error handling
  • Declarative Annotations - @ErrorHandler annotation

Install with Tessl CLI

npx tessl i tessl/maven-dev-langchain4j--langchain4j-agentic@1.11.0

docs

advanced

a2a.md

error-handling.md

human-in-loop.md

persistence.md

index.md

tile.json