ErrorHandlingExample.java
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package software.amazon.lambda.durable.examples.general;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;
import software.amazon.lambda.durable.config.StepConfig;
import software.amazon.lambda.durable.config.StepSemantics;
import software.amazon.lambda.durable.exception.StepFailedException;
import software.amazon.lambda.durable.exception.StepInterruptedException;
import software.amazon.lambda.durable.retry.RetryStrategies;
/**
* Example demonstrating error handling patterns with the Durable Execution SDK.
*
* <p>This example shows how to handle:
*
* <ul>
* <li>{@link StepFailedException} - when a step exhausts all retry attempts
* <li>{@link StepInterruptedException} - when an AT_MOST_ONCE step is interrupted
* <li>Custom exceptions - original exception types are preserved and can be caught directly
* </ul>
*
* <p>Note: {@code NonDeterministicExecutionException} is thrown by the SDK when code changes between executions (e.g.,
* step order/names changed). It should be fixed in code, not caught.
*/
public class ErrorHandlingExample extends DurableHandler<Object, String> {
private static final Logger logger = LoggerFactory.getLogger(ErrorHandlingExample.class);
/** Custom exception to demonstrate that original exception types are preserved across checkpoints. */
public static class ServiceUnavailableException extends RuntimeException {
private String serviceName;
/** Default constructor required for Jackson deserialization. */
public ServiceUnavailableException() {
super();
}
public ServiceUnavailableException(String serviceName, String message) {
super(message);
this.serviceName = serviceName;
}
public String getServiceName() {
return serviceName;
}
}
@Override
public String handleRequest(Object input, DurableContext context) {
// Example 1: Catching a custom exception type with fallback logic
// The SDK preserves the original exception type, so you can catch specific exceptions directly.
// NOTE: Exception type needs to be serializable by your SerDes implementation.
String primaryResult;
try {
primaryResult = context.step(
"call-primary-service",
String.class,
stepCtx -> {
throw new ServiceUnavailableException("primary-api", "Primary service unavailable");
},
StepConfig.builder()
.retryStrategy(RetryStrategies.Presets.NO_RETRY)
.build());
} catch (ServiceUnavailableException e) {
// Catch the specific custom exception type - the SDK reconstructs the original exception
logger.warn("Service '{}' unavailable, using fallback: {}", e.getServiceName(), e.getMessage());
primaryResult = context.step("call-fallback-service", String.class, stepCtx -> "fallback-result");
}
// Example 2: Handling StepInterruptedException for AT_MOST_ONCE operations
// StepInterruptedException is thrown when an AT_MOST_ONCE step was started
// but the function was interrupted before the step completed.
// In normal execution, this step succeeds. The catch block handles the
// interruption scenario that occurs during replay after an unexpected termination.
String paymentResult;
try {
paymentResult = context.step(
"charge-payment",
String.class,
stepCtx -> "payment-" + input,
StepConfig.builder()
.semantics(StepSemantics.AT_MOST_ONCE_PER_RETRY)
.build());
} catch (StepInterruptedException e) {
logger.warn(
"Payment step interrupted, checking external status: {}",
e.getOperation().id());
// In real code: check payment provider for transaction status
// If payment went through, return success; otherwise, handle appropriately
paymentResult = context.step("verify-payment-status", String.class, stepCtx -> "verified-payment");
}
return "Completed: " + primaryResult + ", " + paymentResult;
}
}