1. java
  2. /exceptions
  3. /best-practices

Exception Handling Best Practices in Java

Exception Handling Best Practices

Effective exception handling is crucial for building robust, maintainable Java applications. Following established best practices ensures your applications handle errors gracefully, provide meaningful diagnostics, and maintain system stability. This guide covers essential patterns and practices for professional exception handling.

Core Principles

Fundamental Guidelines:

  1. Fail Fast: Detect and report errors as early as possible
  2. Fail Safe: Ensure system stability even when errors occur
  3. Provide Context: Include meaningful information for debugging
  4. Be Specific: Use precise exception types rather than generic ones
  5. Handle Appropriately: Different exceptions require different responses
  6. Clean Up Resources: Always release resources in finally blocks or try-with-resources
public class CorePrinciples {
    
    // GOOD: Fail fast with specific validation
    public void processUser(User user) {
        if (user == null) {
            throw new IllegalArgumentException("User cannot be null");
        }
        if (user.getEmail() == null || user.getEmail().trim().isEmpty()) {
            throw new IllegalArgumentException("User email is required");
        }
        if (!isValidEmail(user.getEmail())) {
            throw new IllegalArgumentException("Invalid email format: " + user.getEmail());
        }
        
        // Process user...
    }
    
    // BAD: Late failure, generic error
    public void processUserBad(User user) {
        try {
            // Processing starts, then fails deep in the call stack
            String email = user.getEmail().toLowerCase(); // NPE here
            sendEmail(email); // Or here
        } catch (Exception e) {
            throw new RuntimeException("Something went wrong"); // Lost context
        }
    }
    
    // GOOD: Resource management with try-with-resources
    public String readFile(String filename) throws IOException {
        try (BufferedReader reader = Files.newBufferedReader(Paths.get(filename))) {
            return reader.lines().collect(Collectors.joining("\n"));
        }
    }
    
    // BAD: Manual resource management
    public String readFileBad(String filename) throws IOException {
        BufferedReader reader = null;
        try {
            reader = Files.newBufferedReader(Paths.get(filename));
            return reader.lines().collect(Collectors.joining("\n"));
        } finally {
            if (reader != null) {
                reader.close(); // What if this throws?
            }
        }
    }
    
    private boolean isValidEmail(String email) {
        return email.contains("@") && email.contains(".");
    }
    
    private void sendEmail(String email) {
        System.out.println("Sending email to: " + email);
    }
    
    static class User {
        private String email;
        public String getEmail() { return email; }
        public User(String email) { this.email = email; }
    }
}

Exception Type Selection

Choose the right exception type for the situation:

public class ExceptionTypeSelection {
    
    // Use checked exceptions for recoverable conditions
    public void saveToDatabase(String data) throws DataAccessException {
        if (!isDatabaseConnected()) {
            throw new DataAccessException("Database connection lost - retry possible");
        }
        // Save data...
    }
    
    // Use unchecked exceptions for programming errors
    public void setAge(int age) {
        if (age < 0 || age > 150) {
            throw new IllegalArgumentException("Invalid age: " + age + ". Must be 0-150");
        }
        this.age = age;
    }
    
    // Use specific exceptions rather than generic ones
    public User findUser(String id) throws UserNotFoundException {
        User user = userRepository.findById(id);
        if (user == null) {
            throw new UserNotFoundException("User not found: " + id);
        }
        return user;
    }
    
    // AVOID: Generic exceptions that hide intent
    public User findUserBad(String id) throws Exception {  // Too generic
        User user = userRepository.findById(id);
        if (user == null) {
            throw new Exception("Error"); // Meaningless
        }
        return user;
    }
    
    // Use standard exceptions when appropriate
    public void processArray(int[] array, int index) {
        Objects.requireNonNull(array, "Array cannot be null");
        
        if (index < 0 || index >= array.length) {
            throw new IndexOutOfBoundsException("Index " + index + " out of bounds for array length " + array.length);
        }
        
        // Process array...
    }
    
    // Chain exceptions to preserve context
    public void performComplexOperation() throws ServiceException {
        try {
            databaseOperation();
        } catch (SQLException e) {
            throw new ServiceException("Failed to perform database operation", e);
        }
    }
    
    private int age;
    private boolean isDatabaseConnected() { return true; }
    private void databaseOperation() throws SQLException { 
        throw new SQLException("Connection timeout");
    }
    
    // Mock dependencies
    static class UserRepository {
        User findById(String id) { return null; }
    }
    
    static class User {}
    static class UserNotFoundException extends Exception {
        public UserNotFoundException(String message) { super(message); }
    }
    
    static class DataAccessException extends Exception {
        public DataAccessException(String message) { super(message); }
    }
    
    static class ServiceException extends Exception {
        public ServiceException(String message, Throwable cause) { super(message, cause); }
    }
    
    private UserRepository userRepository = new UserRepository();
}

Proper Exception Handling Patterns

import java.util.*;
import java.util.concurrent.*;
import java.util.logging.Logger;

public class ExceptionHandlingPatterns {
    
    private static final Logger logger = Logger.getLogger(ExceptionHandlingPatterns.class.getName());
    
    // 1. Handle exceptions at the right level
    public class UserController {
        public ResponseEntity<?> createUser(UserRequest request) {
            try {
                User user = userService.createUser(request);
                return ResponseEntity.ok(user);
            } catch (DuplicateUserException e) {
                return ResponseEntity.status(409).body("User already exists: " + e.getMessage());
            } catch (InvalidUserDataException e) {
                return ResponseEntity.badRequest().body("Validation failed: " + e.getMessage());
            } catch (ServiceException e) {
                logger.severe("Service error creating user: " + e.getMessage());
                return ResponseEntity.status(500).body("Internal server error");
            }
        }
    }
    
    // 2. Use specific catch blocks
    public void processPayment(PaymentRequest request) {
        try {
            paymentService.processPayment(request);
        } catch (InsufficientFundsException e) {
            // Handle insufficient funds - might suggest alternative payment methods
            notifyUser("Insufficient funds. Available: $" + e.getAvailableAmount());
        } catch (PaymentTimeoutException e) {
            // Handle timeout - might retry or suggest trying later
            scheduleRetry(request, e.getRetryAfter());
        } catch (InvalidCardException e) {
            // Handle invalid card - prompt for different card
            requestNewPaymentMethod("Invalid card: " + e.getReason());
        } catch (PaymentException e) {
            // Handle other payment issues
            logger.warning("Payment failed: " + e.getMessage());
            notifyUser("Payment failed. Please try again later.");
        }
    }
    
    // 3. Don't swallow exceptions without good reason
    public void performBackgroundTask() {
        try {
            riskyOperation();
        } catch (Exception e) {
            // GOOD: Log the exception even if not rethrowing
            logger.severe("Background task failed: " + e.getMessage());
            // Optionally notify monitoring system
            notifyMonitoringSystem(e);
        }
    }
    
    // BAD: Swallowing exceptions
    public void performBackgroundTaskBad() {
        try {
            riskyOperation();
        } catch (Exception e) {
            // BAD: Silent failure - no one knows what happened
        }
    }
    
    // 4. Use finally blocks for cleanup
    public void processFile(String filename) throws ProcessingException {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(filename);
            // Process file...
        } catch (FileNotFoundException e) {
            throw new ProcessingException("File not found: " + filename, e);
        } catch (IOException e) {
            throw new ProcessingException("Error reading file: " + filename, e);
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    logger.warning("Failed to close file: " + e.getMessage());
                }
            }
        }
    }
    
    // BETTER: Use try-with-resources
    public void processFileImproved(String filename) throws ProcessingException {
        try (FileInputStream fis = new FileInputStream(filename)) {
            // Process file...
        } catch (FileNotFoundException e) {
            throw new ProcessingException("File not found: " + filename, e);
        } catch (IOException e) {
            throw new ProcessingException("Error reading file: " + filename, e);
        }
    }
    
    // 5. Retry pattern with exponential backoff
    public <T> T retryOperation(Supplier<T> operation, int maxAttempts) throws OperationFailedException {
        Exception lastException = null;
        
        for (int attempt = 1; attempt <= maxAttempts; attempt++) {
            try {
                return operation.get();
            } catch (TransientException e) {
                lastException = e;
                
                if (attempt == maxAttempts) {
                    break; // Don't wait after the last attempt
                }
                
                long waitTime = (long) Math.pow(2, attempt - 1) * 1000; // Exponential backoff
                logger.info("Operation failed (attempt " + attempt + "/" + maxAttempts + 
                           "), retrying in " + waitTime + "ms");
                
                try {
                    Thread.sleep(waitTime);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new OperationFailedException("Operation interrupted", ie);
                }
            } catch (Exception e) {
                // Non-transient exception - don't retry
                throw new OperationFailedException("Operation failed permanently", e);
            }
        }
        
        throw new OperationFailedException("Operation failed after " + maxAttempts + " attempts", lastException);
    }
    
    // 6. Circuit breaker pattern
    public static class CircuitBreaker {
        private enum State { CLOSED, OPEN, HALF_OPEN }
        
        private State state = State.CLOSED;
        private int failureCount = 0;
        private long lastFailureTime = 0;
        private final int failureThreshold;
        private final long timeoutDuration;
        
        public CircuitBreaker(int failureThreshold, long timeoutDuration) {
            this.failureThreshold = failureThreshold;
            this.timeoutDuration = timeoutDuration;
        }
        
        public <T> T execute(Supplier<T> operation) throws CircuitBreakerOpenException {
            if (state == State.OPEN) {
                if (System.currentTimeMillis() - lastFailureTime > timeoutDuration) {
                    state = State.HALF_OPEN;
                    failureCount = 0;
                } else {
                    throw new CircuitBreakerOpenException("Circuit breaker is open");
                }
            }
            
            try {
                T result = operation.get();
                onSuccess();
                return result;
            } catch (Exception e) {
                onFailure();
                throw e;
            }
        }
        
        private void onSuccess() {
            failureCount = 0;
            state = State.CLOSED;
        }
        
        private void onFailure() {
            failureCount++;
            lastFailureTime = System.currentTimeMillis();
            
            if (failureCount >= failureThreshold) {
                state = State.OPEN;
            }
        }
    }
    
    // Mock classes and methods
    private void riskyOperation() throws Exception { throw new Exception("Risk!"); }
    private void notifyUser(String message) { System.out.println("User notification: " + message); }
    private void scheduleRetry(PaymentRequest request, long retryAfter) {}
    private void requestNewPaymentMethod(String reason) { System.out.println("Request new payment: " + reason); }
    private void notifyMonitoringSystem(Exception e) { System.out.println("Monitoring alert: " + e.getMessage()); }
    
    static class ResponseEntity<T> {
        public static <T> ResponseEntity<T> ok(T body) { return new ResponseEntity<>(); }
        public static <T> ResponseEntity<T> badRequest() { return new ResponseEntity<>(); }
        public static <T> ResponseEntity<T> status(int status) { return new ResponseEntity<>(); }
        public ResponseEntity<T> body(T body) { return this; }
    }
    
    static class UserRequest {}
    static class User {}
    static class PaymentRequest {}
    static class UserService { User createUser(UserRequest request) { return new User(); } }
    static class PaymentService { void processPayment(PaymentRequest request) {} }
    
    static class DuplicateUserException extends Exception { public DuplicateUserException(String msg) { super(msg); } }
    static class InvalidUserDataException extends Exception { public InvalidUserDataException(String msg) { super(msg); } }
    static class ServiceException extends Exception { public ServiceException(String msg) { super(msg); } }
    static class InsufficientFundsException extends Exception { 
        public InsufficientFundsException(String msg) { super(msg); }
        public double getAvailableAmount() { return 100.0; }
    }
    static class PaymentTimeoutException extends Exception {
        public PaymentTimeoutException(String msg) { super(msg); }
        public long getRetryAfter() { return 5000; }
    }
    static class InvalidCardException extends Exception {
        public InvalidCardException(String msg) { super(msg); }
        public String getReason() { return "Expired"; }
    }
    static class PaymentException extends Exception { public PaymentException(String msg) { super(msg); } }
    static class ProcessingException extends Exception { 
        public ProcessingException(String msg, Throwable cause) { super(msg, cause); }
    }
    static class OperationFailedException extends Exception {
        public OperationFailedException(String msg, Throwable cause) { super(msg, cause); }
    }
    static class TransientException extends RuntimeException {}
    static class CircuitBreakerOpenException extends Exception { public CircuitBreakerOpenException(String msg) { super(msg); } }
    
    private UserService userService = new UserService();
    private PaymentService paymentService = new PaymentService();
}

Logging and Monitoring

import java.util.logging.*;
import java.util.Map;
import java.util.HashMap;

public class ExceptionLoggingPractices {
    
    private static final Logger logger = Logger.getLogger(ExceptionLoggingPractices.class.getName());
    
    // 1. Log at appropriate levels
    public void demonstrateLoggingLevels() {
        try {
            criticalOperation();
        } catch (SecurityException e) {
            // SEVERE: System errors, security issues
            logger.severe("Security violation detected: " + e.getMessage());
        } catch (DataCorruptionException e) {
            // SEVERE: Data integrity issues
            logger.severe("Data corruption detected: " + e.getMessage());
        } catch (ServiceUnavailableException e) {
            // WARNING: Temporary issues, degraded functionality
            logger.warning("Service temporarily unavailable: " + e.getMessage());
        } catch (ValidationException e) {
            // INFO: Expected business logic issues
            logger.info("Validation failed: " + e.getMessage());
        } catch (UserNotFoundException e) {
            // FINE: Debug information
            logger.fine("User lookup failed: " + e.getMessage());
        }
    }
    
    // 2. Include relevant context in log messages
    public void processOrder(String orderId, String customerId) {
        try {
            orderService.processOrder(orderId, customerId);
            logger.info("Order processed successfully: orderId=" + orderId + 
                       ", customerId=" + customerId);
        } catch (Exception e) {
            // Include context in error logs
            logger.severe("Order processing failed: orderId=" + orderId + 
                         ", customerId=" + customerId + 
                         ", error=" + e.getMessage());
        }
    }
    
    // 3. Create structured logging utility
    public static class StructuredLogger {
        private final Logger logger;
        
        public StructuredLogger(Class<?> clazz) {
            this.logger = Logger.getLogger(clazz.getName());
        }
        
        public void logError(String operation, Map<String, Object> context, Exception e) {
            StringBuilder sb = new StringBuilder();
            sb.append("Operation failed: ").append(operation);
            
            if (context != null && !context.isEmpty()) {
                sb.append(" | Context: ");
                context.forEach((key, value) -> 
                    sb.append(key).append("=").append(value).append(" "));
            }
            
            sb.append(" | Error: ").append(e.getMessage());
            
            logger.severe(sb.toString());
            
            // In production, might also send to external monitoring
            if (isCritical(e)) {
                sendToMonitoring(operation, context, e);
            }
        }
        
        private boolean isCritical(Exception e) {
            return e instanceof SecurityException || 
                   e instanceof DataCorruptionException ||
                   e.getMessage().contains("OutOfMemory");
        }
        
        private void sendToMonitoring(String operation, Map<String, Object> context, Exception e) {
            // Send to external monitoring system (e.g., Datadog, New Relic)
            System.out.println("🚨 ALERT: Critical error in " + operation);
        }
    }
    
    // 4. Exception metrics and monitoring
    public static class ExceptionMetrics {
        private final Map<String, Integer> exceptionCounts = new HashMap<>();
        private final Map<String, Long> lastOccurrence = new HashMap<>();
        
        public void recordException(Exception e) {
            String exceptionType = e.getClass().getSimpleName();
            
            exceptionCounts.merge(exceptionType, 1, Integer::sum);
            lastOccurrence.put(exceptionType, System.currentTimeMillis());
            
            // Alert if frequency is too high
            if (exceptionCounts.get(exceptionType) > 10) {
                logger.warning("High frequency of " + exceptionType + ": " + 
                              exceptionCounts.get(exceptionType) + " occurrences");
            }
        }
        
        public void printMetrics() {
            logger.info("Exception Metrics:");
            exceptionCounts.forEach((type, count) -> 
                logger.info("  " + type + ": " + count + " occurrences"));
        }
    }
    
    // 5. Correlation IDs for tracking
    public static class CorrelationIdLogger {
        private static final ThreadLocal<String> correlationId = new ThreadLocal<>();
        private final Logger logger;
        
        public CorrelationIdLogger(Class<?> clazz) {
            this.logger = Logger.getLogger(clazz.getName());
        }
        
        public static void setCorrelationId(String id) {
            correlationId.set(id);
        }
        
        public static void clearCorrelationId() {
            correlationId.remove();
        }
        
        public void logWithCorrelation(Level level, String message, Exception e) {
            String id = correlationId.get();
            String logMessage = (id != null) ? 
                "[" + id + "] " + message : message;
            
            if (e != null) {
                logger.log(level, logMessage, e);
            } else {
                logger.log(level, logMessage);
            }
        }
    }
    
    // Usage example with correlation ID
    public void processRequest(String requestId) {
        CorrelationIdLogger correlationLogger = new CorrelationIdLogger(this.getClass());
        CorrelationIdLogger.setCorrelationId(requestId);
        
        try {
            correlationLogger.logWithCorrelation(Level.INFO, "Processing request", null);
            
            businessLogic();
            
            correlationLogger.logWithCorrelation(Level.INFO, "Request completed successfully", null);
            
        } catch (Exception e) {
            correlationLogger.logWithCorrelation(Level.SEVERE, "Request processing failed", e);
            throw e;
        } finally {
            CorrelationIdLogger.clearCorrelationId();
        }
    }
    
    // Mock methods and classes
    private void criticalOperation() throws Exception { throw new SecurityException("Access denied"); }
    private void businessLogic() throws Exception { throw new ValidationException("Invalid data"); }
    
    static class OrderService { void processOrder(String orderId, String customerId) {} }
    static class SecurityException extends Exception { public SecurityException(String msg) { super(msg); } }
    static class DataCorruptionException extends Exception { public DataCorruptionException(String msg) { super(msg); } }
    static class ServiceUnavailableException extends Exception { public ServiceUnavailableException(String msg) { super(msg); } }
    static class ValidationException extends Exception { public ValidationException(String msg) { super(msg); } }
    static class UserNotFoundException extends Exception { public UserNotFoundException(String msg) { super(msg); } }
    
    private OrderService orderService = new OrderService();
}

Performance Considerations

public class ExceptionPerformancePractices {
    
    // 1. Avoid exceptions for control flow
    public String findUserEmailBad(String userId) {
        try {
            User user = userService.findById(userId);
            return user.getEmail(); // Throws if user is null
        } catch (NullPointerException e) {
            return "[email protected]"; // BAD: Using exception for control flow
        }
    }
    
    public String findUserEmailGood(String userId) {
        User user = userService.findById(userId);
        return user != null ? user.getEmail() : "[email protected]"; // GOOD: Check condition
    }
    
    // 2. Cache exception instances for frequently thrown exceptions
    public static class ValidationUtils {
        private static final IllegalArgumentException NULL_ARGUMENT_EXCEPTION = 
            new IllegalArgumentException("Argument cannot be null");
        
        private static final IllegalArgumentException EMPTY_STRING_EXCEPTION = 
            new IllegalArgumentException("String cannot be empty");
        
        public static void requireNonNull(Object obj) {
            if (obj == null) {
                throw NULL_ARGUMENT_EXCEPTION; // Reuse instance
            }
        }
        
        public static void requireNonEmpty(String str) {
            if (str == null || str.isEmpty()) {
                throw EMPTY_STRING_EXCEPTION; // Reuse instance
            }
        }
        
        // For dynamic messages, create new instances
        public static void requirePositive(int value, String paramName) {
            if (value <= 0) {
                throw new IllegalArgumentException(paramName + " must be positive, got: " + value);
            }
        }
    }
    
    // 3. Minimize stack trace generation when possible
    public static class LightweightException extends RuntimeException {
        public LightweightException(String message) {
            super(message);
        }
        
        // Override fillInStackTrace to avoid expensive stack trace generation
        @Override
        public synchronized Throwable fillInStackTrace() {
            // For high-frequency exceptions where stack trace isn't needed
            return this;
        }
    }
    
    // 4. Use Optional for methods that might not return values
    public Optional<User> findUser(String id) {
        User user = userService.findById(id);
        return Optional.ofNullable(user); // No exception needed
    }
    
    // Instead of throwing exceptions
    public User findUserOrThrow(String id) throws UserNotFoundException {
        return findUser(id).orElseThrow(() -> 
            new UserNotFoundException("User not found: " + id));
    }
    
    // 5. Batch validation to avoid multiple exceptions
    public static class ValidationResult {
        private final List<String> errors = new ArrayList<>();
        
        public ValidationResult addError(String error) {
            errors.add(error);
            return this;
        }
        
        public boolean hasErrors() {
            return !errors.isEmpty();
        }
        
        public List<String> getErrors() {
            return Collections.unmodifiableList(errors);
        }
        
        public void throwIfInvalid() throws ValidationException {
            if (hasErrors()) {
                throw new ValidationException("Validation failed: " + String.join(", ", errors));
            }
        }
    }
    
    public void validateUser(User user) throws ValidationException {
        ValidationResult result = new ValidationResult();
        
        if (user.getName() == null || user.getName().trim().isEmpty()) {
            result.addError("Name is required");
        }
        
        if (user.getEmail() == null || !user.getEmail().contains("@")) {
            result.addError("Valid email is required");
        }
        
        if (user.getAge() < 0 || user.getAge() > 150) {
            result.addError("Age must be between 0 and 150");
        }
        
        result.throwIfInvalid(); // Single exception with all errors
    }
    
    // 6. Performance testing
    public void demonstratePerformanceImpact() {
        int iterations = 100_000;
        
        // Test 1: Normal control flow
        long start = System.nanoTime();
        for (int i = 0; i < iterations; i++) {
            if (i % 2 == 0) {
                // Normal path
            }
        }
        long normalTime = System.nanoTime() - start;
        
        // Test 2: Exception-based control flow
        start = System.nanoTime();
        for (int i = 0; i < iterations; i++) {
            try {
                if (i % 2 == 0) {
                    throw new RuntimeException("Control flow exception");
                }
            } catch (RuntimeException e) {
                // Exception path
            }
        }
        long exceptionTime = System.nanoTime() - start;
        
        System.out.println("Normal control flow: " + (normalTime / 1_000_000) + " ms");
        System.out.println("Exception control flow: " + (exceptionTime / 1_000_000) + " ms");
        System.out.println("Exception overhead: " + (exceptionTime / normalTime) + "x slower");
    }
    
    // Mock classes
    static class User {
        private String name, email;
        private int age;
        public String getName() { return name; }
        public String getEmail() { return email; }
        public int getAge() { return age; }
        public User(String name, String email, int age) { this.name = name; this.email = email; this.age = age; }
    }
    
    static class UserService { 
        public User findById(String id) { return new User("John", "[email protected]", 30); }
    }
    
    static class UserNotFoundException extends Exception { 
        public UserNotFoundException(String msg) { super(msg); }
    }
    
    static class ValidationException extends Exception {
        public ValidationException(String msg) { super(msg); }
    }
    
    private UserService userService = new UserService();
}

Testing Exception Handling

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

public class ExceptionTestingPractices {
    
    // 1. Test that exceptions are thrown when expected
    @Test
    public void shouldThrowExceptionForInvalidInput() {
        UserService userService = new UserService();
        
        assertThrows(IllegalArgumentException.class, () -> {
            userService.createUser(null, "[email protected]");
        });
        
        assertThrows(IllegalArgumentException.class, () -> {
            userService.createUser("John", null);
        });
    }
    
    // 2. Test exception messages
    @Test
    public void shouldThrowExceptionWithCorrectMessage() {
        UserService userService = new UserService();
        
        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
            userService.createUser("", "[email protected]");
        });
        
        assertEquals("Name cannot be empty", exception.getMessage());
    }
    
    // 3. Test exception handling behavior
    @Test
    public void shouldHandleExceptionGracefully() {
        UserRepository mockRepository = Mockito.mock(UserRepository.class);
        Mockito.when(mockRepository.save(Mockito.any())).thenThrow(new DatabaseException("Connection failed"));
        
        UserService userService = new UserService(mockRepository);
        
        assertThrows(ServiceException.class, () -> {
            userService.createUser("John", "[email protected]");
        });
        
        // Verify the exception was handled and logged
        // In real tests, you might verify log entries or monitoring calls
    }
    
    // 4. Test resource cleanup
    @Test
    public void shouldCleanupResourcesOnException() {
        FileProcessor processor = new FileProcessor();
        MockResource resource = new MockResource();
        
        try {
            processor.processWithResource(resource, true); // true = throw exception
            fail("Should have thrown an exception");
        } catch (ProcessingException e) {
            // Expected
        }
        
        assertTrue(resource.isClosed(), "Resource should be closed even when exception occurs");
    }
    
    // 5. Test retry mechanisms
    @Test
    public void shouldRetryOnTransientFailure() {
        RetryableService service = Mockito.mock(RetryableService.class);
        
        // Fail twice, then succeed
        Mockito.when(service.performOperation())
            .thenThrow(new TransientException("Temporary failure"))
            .thenThrow(new TransientException("Temporary failure"))
            .thenReturn("Success");
        
        RetryableOperationExecutor executor = new RetryableOperationExecutor(3);
        String result = executor.executeWithRetry(service::performOperation);
        
        assertEquals("Success", result);
        Mockito.verify(service, Mockito.times(3)).performOperation();
    }
    
    // Mock classes for testing
    static class UserService {
        private final UserRepository repository;
        
        public UserService() {
            this.repository = new UserRepository();
        }
        
        public UserService(UserRepository repository) {
            this.repository = repository;
        }
        
        public User createUser(String name, String email) throws ServiceException {
            if (name == null) {
                throw new IllegalArgumentException("Name cannot be null");
            }
            if (name.trim().isEmpty()) {
                throw new IllegalArgumentException("Name cannot be empty");
            }
            if (email == null) {
                throw new IllegalArgumentException("Email cannot be null");
            }
            
            User user = new User(name, email);
            
            try {
                return repository.save(user);
            } catch (DatabaseException e) {
                throw new ServiceException("Failed to create user", e);
            }
        }
    }
    
    static class FileProcessor {
        public void processWithResource(MockResource resource, boolean shouldThrow) throws ProcessingException {
            try {
                resource.open();
                if (shouldThrow) {
                    throw new ProcessingException("Simulated processing error");
                }
            } finally {
                resource.close();
            }
        }
    }
    
    static class RetryableOperationExecutor {
        private final int maxAttempts;
        
        public RetryableOperationExecutor(int maxAttempts) {
            this.maxAttempts = maxAttempts;
        }
        
        public String executeWithRetry(Supplier<String> operation) {
            for (int attempt = 1; attempt <= maxAttempts; attempt++) {
                try {
                    return operation.get();
                } catch (TransientException e) {
                    if (attempt == maxAttempts) {
                        throw new RuntimeException("Max retry attempts exceeded", e);
                    }
                }
            }
            throw new RuntimeException("Should not reach here");
        }
    }
    
    static class MockResource {
        private boolean closed = false;
        
        public void open() {}
        public void close() { closed = true; }
        public boolean isClosed() { return closed; }
    }
    
    static class User {
        private final String name, email;
        public User(String name, String email) { this.name = name; this.email = email; }
    }
    
    static class UserRepository {
        public User save(User user) throws DatabaseException {
            return user;
        }
    }
    
    interface RetryableService {
        String performOperation() throws TransientException;
    }
    
    static class DatabaseException extends Exception {
        public DatabaseException(String msg) { super(msg); }
    }
    
    static class ServiceException extends Exception {
        public ServiceException(String msg, Throwable cause) { super(msg, cause); }
    }
    
    static class ProcessingException extends Exception {
        public ProcessingException(String msg) { super(msg); }
    }
    
    static class TransientException extends RuntimeException {
        public TransientException(String msg) { super(msg); }
    }
    
    interface Supplier<T> {
        T get() throws TransientException;
    }
}

Summary

Core Principles:

  • Fail Fast: Validate inputs and detect errors early
  • Be Specific: Use precise exception types with meaningful messages
  • Preserve Context: Chain exceptions to maintain error history
  • Clean Up: Always release resources properly
  • Handle Appropriately: Different exceptions need different responses

Exception Selection:

  • Checked Exceptions: For recoverable conditions callers must handle
  • Unchecked Exceptions: For programming errors and unrecoverable conditions
  • Standard Exceptions: Use built-in exceptions when appropriate
  • Custom Exceptions: For domain-specific error conditions

Handling Patterns:

  • Specific Catch Blocks: Handle different exceptions differently
  • Proper Cleanup: Use try-with-resources or finally blocks
  • Retry Logic: Implement retry with exponential backoff for transient failures
  • Circuit Breakers: Prevent cascade failures in distributed systems

Logging and Monitoring:

  • Appropriate Levels: SEVERE for critical issues, WARNING for degraded service
  • Rich Context: Include operation details, correlation IDs, relevant data
  • Structured Logging: Use consistent format for better analysis
  • Metrics: Track exception frequency and patterns

Performance:

  • Avoid Control Flow: Don't use exceptions for normal program flow
  • Cache Instances: Reuse exception instances for frequent cases
  • Batch Validation: Collect multiple errors before throwing
  • Lightweight Exceptions: Skip stack traces for high-frequency cases

Testing:

  • Test Exception Conditions: Verify exceptions are thrown when expected
  • Verify Messages: Check that error messages are meaningful
  • Test Handling: Ensure exceptions are handled appropriately
  • Resource Cleanup: Verify resources are released on exceptions

Following these best practices leads to robust, maintainable applications with clear error handling that aids both debugging and user experience.