Error Handling
Definition

Exception Types in Java

Exception Anti-Patterns
// ANTI-PATTERN 1: Catching generic Exception
try {
processOrder(order);
} catch (Exception e) {
// Catches everything including bugs!
log.error("Error", e);
}
// ANTI-PATTERN 2: Empty catch block
try {
file.close();
} catch (IOException e) {
// Swallowed! Debugging nightmare
}
// ANTI-PATTERN 3: Catch and rethrow without adding value
try {
repository.save(order);
} catch (SQLException e) {
throw e; // Pointless
}
// ANTI-PATTERN 4: Using exceptions for flow control
try {
User user = getUser(id);
} catch (UserNotFoundException e) {
user = createUser(id); // Expected case treated as exception!
}
// ANTI-PATTERN 5: Logging and throwing
try {
process();
} catch (ProcessingException e) {
log.error("Failed", e); // Logged here
throw e; // And logged again by caller!
}
// ANTI-PATTERN 6: Losing stack trace
try {
parse(data);
} catch (ParseException e) {
throw new RuntimeException("Parse failed"); // Lost original cause!
}
Best Practices
// PRACTICE 1: Catch specific exceptions
try {
saveOrder(order);
} catch (ValidationException e) {
return Response.badRequest(e.getMessage());
} catch (DuplicateOrderException e) {
return Response.conflict("Order already exists");
}
// PRACTICE 2: Preserve the cause
try {
parseConfig(file);
} catch (ParseException e) {
throw new ConfigurationException("Invalid config file: " + file, e);
// ↑ preserve!
}
// PRACTICE 3: Use try-with-resources
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
// Resources automatically closed
} catch (SQLException e) {
throw new DataAccessException("Query failed", e);
}
// PRACTICE 4: Return Optional instead of throwing for "not found"
public Optional<User> findById(String id) {
User user = repository.findById(id);
return Optional.ofNullable(user);
}
// PRACTICE 5: Create meaningful custom exceptions
public class InsufficientFundsException extends BusinessException {
private final Money requested;
private final Money available;
public InsufficientFundsException(Money requested, Money available) {
super(String.format(
"Insufficient funds: requested %s, available %s",
requested, available));
this.requested = requested;
this.available = available;
}
// Getters for programmatic access
}
Exception Design
// Designing exception hierarchies
// Base exception for your domain
public abstract class OrderException extends RuntimeException {
public OrderException(String message) {
super(message);
}
public OrderException(String message, Throwable cause) {
super(message, cause);
}
}
// Specific exceptions
public class OrderNotFoundException extends OrderException {
private final String orderId;
public OrderNotFoundException(String orderId) {
super("Order not found: " + orderId);
this.orderId = orderId;
}
public String getOrderId() { return orderId; }
}
public class InvalidOrderStateException extends OrderException {
private final OrderStatus currentStatus;
private final OrderStatus requiredStatus;
public InvalidOrderStateException(OrderStatus current, OrderStatus required) {
super(String.format(
"Invalid order state: current=%s, required=%s",
current, required));
this.currentStatus = current;
this.requiredStatus = required;
}
}
// Checked vs Unchecked decision:
// - Caller can reasonably handle it? → Checked
// - Programming error? → Unchecked
// - Modern preference: Unchecked for everything
Error Handling Patterns
// Pattern 1: Result type (instead of exceptions)
public class Result<T> {
private final T value;
private final Error error;
private final boolean success;
public static <T> Result<T> success(T value) {
return new Result<>(value, null, true);
}
public static <T> Result<T> failure(Error error) {
return new Result<>(null, error, false);
}
}
public Result<Order> processOrder(OrderRequest request) {
if (!validator.isValid(request)) {
return Result.failure(new ValidationError(validator.getErrors()));
}
Order order = createOrder(request);
return Result.success(order);
}
// Pattern 2: Either type
public Either<Error, Order> processOrder(OrderRequest request) {
return validator.validate(request)
.flatMap(this::checkInventory)
.flatMap(this::processPayment)
.map(this::createOrder);
}
// Pattern 3: Error handler/callback
public interface ErrorHandler {
void handle(Exception e);
}
public void process(Data data, ErrorHandler onError) {
try {
doProcess(data);
} catch (ProcessingException e) {
onError.handle(e);
}
}
API Error Responses
// Consistent error responses for APIs
// Standard error response
public record ErrorResponse(
String code,
String message,
Map<String, String> details,
Instant timestamp
) {}
// Global exception handler (Spring)
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ValidationException.class)
public ResponseEntity<ErrorResponse> handleValidation(ValidationException e) {
return ResponseEntity
.badRequest()
.body(new ErrorResponse(
"VALIDATION_ERROR",
e.getMessage(),
e.getFieldErrors(),
Instant.now()
));
}
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException e) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse(
"NOT_FOUND",
e.getMessage(),
Map.of("resource", e.getResourceId()),
Instant.now()
));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleUnexpected(Exception e) {
log.error("Unexpected error", e);
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse(
"INTERNAL_ERROR",
"An unexpected error occurred",
Map.of(),
Instant.now()
));
}
}
Tips & Tricks
