Your banking app needs to signal "insufficient funds". Using generic Exception loses meaning. Custom exceptions like InsufficientFundsException carry the balance and requested amount - meaningful error information.

Simple custom exception

Create your own exception type.

SimpleCustom.java
// Creating Basic Custom Exceptions

public class SimpleCustom {
    public static void main(String[] args) {
        System.out.println("=== Basic Custom Exceptions ===\n");

        // Simple custom exception
        System.out.println("--- Simple Custom Exception ---");

        try {
            validateAge(-5);
        } catch (InvalidAgeException e) {
            System.out.println("Caught: " + e.getMessage());
        }

        // Custom exception with valid input
        System.out.println("\n--- Valid Input ---");

        try {
            validateAge(25);
            System.out.println("Age 25 is valid!");
        } catch (InvalidAgeException e) {
            System.out.println("Error: " + e.getMessage());
        }

        // Another custom exception
        System.out.println("\n--- Another Custom Exception ---");

        try {
            validateUsername("ab");
        } catch (InvalidUsernameException e) {
            System.out.println("Caught: " + e.getMessage());
        }

        // Multiple custom exceptions
        System.out.println("\n--- Multiple Custom Exceptions ---");

        String[] usernames = {"alice123", "x", "bob_smith", ""};
        int[] ages = {25, -1, 200, 30};

        for (int i = 0; i < usernames.length; i++) {
            try {
                validateUsername(usernames[i]);
                validateAge(ages[i]);
                System.out.println("✓ Valid: " + usernames[i] + ", age " + ages[i]);
            } catch (InvalidUsernameException e) {
                System.out.println("✗ Username error: " + e.getMessage());
            } catch (InvalidAgeException e) {
                System.out.println("✗ Age error: " + e.getMessage());
            }
        }

        System.out.println("\n=== Key Points ===");
        System.out.println("""
            1. Extend RuntimeException for unchecked
            2. Extend Exception for checked
            3. Call super(message) to set message
            4. Name should end with 'Exception'
            5. Name should describe the error clearly
            """);
    }

    static void validateAge(int age) {
        if (age < 0) {
            throw new InvalidAgeException("Age cannot be negative: " + age);
        }
        if (age > 150) {
            throw new InvalidAgeException("Age cannot exceed 150: " + age);
        }
    }

    static void validateUsername(String username) {
        if (username == null || username.isEmpty()) {
            throw new InvalidUsernameException("Username cannot be empty");
        }
        if (username.length() < 3) {
            throw new InvalidUsernameException("Username must be at least 3 characters: " + username);
        }
    }
}

// Simple custom exception - unchecked
class InvalidAgeException extends RuntimeException {
    public InvalidAgeException(String message) {
        super(message);
    }
}

// Another simple custom exception
class InvalidUsernameException extends RuntimeException {
    public InvalidUsernameException(String message) {
        super(message);
    }
}

//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//

Extend RuntimeException (unchecked) or Exception (checked).

custom exception Your own exception class. Meaningful name, specific to your domain.

Exception with data

Include relevant data in the exception.

ExceptionWithData.java
// Custom Exceptions with Additional Data

public class ExceptionWithData {
    public static void main(String[] args) {
        System.out.println("=== Custom Exceptions with Data ===\n");

        // Exception with numeric data
        System.out.println("--- Exception with Numeric Data ---");

        double transferAmount = ;
        try {
            transferMoney(100.00, transferAmount);
        } catch (InsufficientFundsException e) {
            System.out.println("Transfer failed!");
            System.out.println("  Message: " + e.getMessage());
            System.out.println("  Balance: $" + e.getBalance());
            System.out.println("  Requested: $" + e.getRequestedAmount());
            System.out.println("  Shortfall: $" + e.getShortfall());
        }

        // Exception with string data
        System.out.println("\n--- Exception with String Data ---");

        try {
            findUser("unknown_user");
        } catch (UserNotFoundException e) {
            System.out.println("User lookup failed!");
            System.out.println("  Message: " + e.getMessage());
            System.out.println("  Username: " + e.getUsername());
            System.out.println("  Search time: " + e.getSearchTimeMs() + "ms");
        }

        // Exception with enum data
        System.out.println("\n--- Exception with Enum Data ---");

        try {
            validateInput("", "email");
        } catch (ValidationException e) {
            System.out.println("Validation failed!");
            System.out.println("  Message: " + e.getMessage());
            System.out.println("  Field: " + e.getFieldName());
            System.out.println("  Error type: " + e.getErrorType());
            System.out.println("  Is required error? " + e.isRequiredError());
        }

        // Using exception data for recovery
        System.out.println("\n--- Using Data for Recovery ---");

        double balance = 100.00;
        double[] amounts = {50.00, 75.00, 30.00};

        for (double amount : amounts) {
            try {
                withdraw(balance, amount);
                balance -= amount;
                System.out.println("✓ Withdrew $" + amount + ", new balance: $" + balance);
            } catch (InsufficientFundsException e) {
                System.out.println("✗ Cannot withdraw $" + amount);
                System.out.println("  Suggestion: withdraw up to $" + e.getBalance());
            }
        }

        System.out.println("\n=== Key Points ===");
        System.out.println("""
            1. Add fields to store relevant error data
            2. Provide getters to access the data
            3. Include data in the message for readability
            4. Caller can use data for error recovery
            5. Consider computed properties (like shortfall)
            """);
    }

    static void transferMoney(double balance, double amount) {
        if (amount > balance) {
            throw new InsufficientFundsException(balance, amount);
        }
        System.out.println("Transfer of $" + amount + " successful");
    }

    static void withdraw(double balance, double amount) {
        if (amount > balance) {
            throw new InsufficientFundsException(balance, amount);
        }
    }

    static void findUser(String username) {
        // Simulate search with a fixed duration
        long searchTime = 50;

        // User not found
        throw new UserNotFoundException(username, searchTime);
    }

    static void validateInput(String value, String fieldName) {
        if (value == null || value.isBlank()) {
            throw new ValidationException(fieldName, ValidationErrorType.REQUIRED);
        }
    }
}

// Exception with numeric data
class InsufficientFundsException extends RuntimeException {
    private final double balance;
    private final double requestedAmount;

    public InsufficientFundsException(double balance, double requestedAmount) {
        super(String.format(
            "Insufficient funds: balance=$%.2f, requested=$%.2f",
            balance, requestedAmount
        ));
        this.balance = balance;
        this.requestedAmount = requestedAmount;
    }

    public double getBalance() {
        return balance;
    }

    public double getRequestedAmount() {
        return requestedAmount;
    }

    public double getShortfall() {
        return requestedAmount - balance;
    }
}

// Exception with string data
class UserNotFoundException extends RuntimeException {
    private final String username;
    private final long searchTimeMs;

    public UserNotFoundException(String username, long searchTimeMs) {
        super("User not found: " + username);
        this.username = username;
        this.searchTimeMs = searchTimeMs;
    }

    public String getUsername() {
        return username;
    }

    public long getSearchTimeMs() {
        return searchTimeMs;
    }
}

// Validation error type enum
enum ValidationErrorType {
    REQUIRED,
    INVALID_FORMAT,
    TOO_SHORT,
    TOO_LONG
}

// Exception with enum data
class ValidationException extends RuntimeException {
    private final String fieldName;
    private final ValidationErrorType errorType;

    public ValidationException(String fieldName, ValidationErrorType errorType) {
        super(fieldName + ": " + errorType.name().toLowerCase().replace('_', ' '));
        this.fieldName = fieldName;
        this.errorType = errorType;
    }

    public String getFieldName() {
        return fieldName;
    }

    public ValidationErrorType getErrorType() {
        return errorType;
    }

    public boolean isRequiredError() {
        return errorType == ValidationErrorType.REQUIRED;
    }
}

//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
// Custom Exceptions with Additional Data

public class ExceptionWithData {
    public static void main(String[] args) {
        System.out.println("=== Custom Exceptions with Data ===\n");

        // Exception with numeric data
        System.out.println("--- Exception with Numeric Data ---");

        double transferAmount = ;
        try {
            transferMoney(100.00, transferAmount);
        } catch (InsufficientFundsException e) {
            System.out.println("Transfer failed!");
            System.out.println("  Message: " + e.getMessage());
            System.out.println("  Balance: $" + e.getBalance());
            System.out.println("  Requested: $" + e.getRequestedAmount());
            System.out.println("  Shortfall: $" + e.getShortfall());
        }

        // Exception with string data
        System.out.println("\n--- Exception with String Data ---");

        try {
            findUser("unknown_user");
        } catch (UserNotFoundException e) {
            System.out.println("User lookup failed!");
            System.out.println("  Message: " + e.getMessage());
            System.out.println("  Username: " + e.getUsername());
            System.out.println("  Search time: " + e.getSearchTimeMs() + "ms");
        }

        // Exception with enum data
        System.out.println("\n--- Exception with Enum Data ---");

        try {
            validateInput("", "email");
        } catch (ValidationException e) {
            System.out.println("Validation failed!");
            System.out.println("  Message: " + e.getMessage());
            System.out.println("  Field: " + e.getFieldName());
            System.out.println("  Error type: " + e.getErrorType());
            System.out.println("  Is required error? " + e.isRequiredError());
        }

        // Using exception data for recovery
        System.out.println("\n--- Using Data for Recovery ---");

        double balance = 100.00;
        double[] amounts = {50.00, 75.00, 30.00};

        for (double amount : amounts) {
            try {
                withdraw(balance, amount);
                balance -= amount;
                System.out.println("✓ Withdrew $" + amount + ", new balance: $" + balance);
            } catch (InsufficientFundsException e) {
                System.out.println("✗ Cannot withdraw $" + amount);
                System.out.println("  Suggestion: withdraw up to $" + e.getBalance());
            }
        }

        System.out.println("\n=== Key Points ===");
        System.out.println("""
            1. Add fields to store relevant error data
            2. Provide getters to access the data
            3. Include data in the message for readability
            4. Caller can use data for error recovery
            5. Consider computed properties (like shortfall)
            """);
    }

    static void transferMoney(double balance, double amount) {
        if (amount > balance) {
            throw new InsufficientFundsException(balance, amount);
        }
        System.out.println("Transfer of $" + amount + " successful");
    }

    static void withdraw(double balance, double amount) {
        if (amount > balance) {
            throw new InsufficientFundsException(balance, amount);
        }
    }

    static void findUser(String username) {
        // Simulate search with a fixed duration
        long searchTime = 50;

        // User not found
        throw new UserNotFoundException(username, searchTime);
    }

    static void validateInput(String value, String fieldName) {
        if (value == null || value.isBlank()) {
            throw new ValidationException(fieldName, ValidationErrorType.REQUIRED);
        }
    }
}

// Exception with numeric data
class InsufficientFundsException extends RuntimeException {
    private final double balance;
    private final double requestedAmount;

    public InsufficientFundsException(double balance, double requestedAmount) {
        super(String.format(
            "Insufficient funds: balance=$%.2f, requested=$%.2f",
            balance, requestedAmount
        ));
        this.balance = balance;
        this.requestedAmount = requestedAmount;
    }

    public double getBalance() {
        return balance;
    }

    public double getRequestedAmount() {
        return requestedAmount;
    }

    public double getShortfall() {
        return requestedAmount - balance;
    }
}

// Exception with string data
class UserNotFoundException extends RuntimeException {
    private final String username;
    private final long searchTimeMs;

    public UserNotFoundException(String username, long searchTimeMs) {
        super("User not found: " + username);
        this.username = username;
        this.searchTimeMs = searchTimeMs;
    }

    public String getUsername() {
        return username;
    }

    public long getSearchTimeMs() {
        return searchTimeMs;
    }
}

// Validation error type enum
enum ValidationErrorType {
    REQUIRED,
    INVALID_FORMAT,
    TOO_SHORT,
    TOO_LONG
}

// Exception with enum data
class ValidationException extends RuntimeException {
    private final String fieldName;
    private final ValidationErrorType errorType;

    public ValidationException(String fieldName, ValidationErrorType errorType) {
        super(fieldName + ": " + errorType.name().toLowerCase().replace('_', ' '));
        this.fieldName = fieldName;
        this.errorType = errorType;
    }

    public String getFieldName() {
        return fieldName;
    }

    public ValidationErrorType getErrorType() {
        return errorType;
    }

    public boolean isRequiredError() {
        return errorType == ValidationErrorType.REQUIRED;
    }
}

//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//

Add fields, constructor, getters. Carry context about what went wrong.

Exception hierarchy

Organize related exceptions into a hierarchy.

ExceptionHierarchy.java
// Building an Exception Class Hierarchy

public class ExceptionHierarchy {
    public static void main(String[] args) {
        System.out.println("=== Exception Hierarchy ===\n");

        // Show the hierarchy
        System.out.println("--- Our Custom Hierarchy ---");
        System.out.println("""
            AppException (base)
            ├── DataException
            │   ├── DataNotFoundException
            │   └── DataValidationException
            └── ServiceException
                ├── AuthenticationException
                └── AuthorizationException
            """);

        // Catch specific exception
        System.out.println("--- Catch Specific Exception ---");

        try {
            findProduct(999);
        } catch (DataNotFoundException e) {
            System.out.println("Caught DataNotFoundException: " + e.getMessage());
            System.out.println("  Entity: " + e.getEntityType());
            System.out.println("  ID: " + e.getEntityId());
        }

        // Catch parent exception
        System.out.println("\n--- Catch Parent Exception (DataException) ---");

        try {
            validateProduct("", -10);
        } catch (DataException e) {
            System.out.println("Caught DataException: " + e.getClass().getSimpleName());
            System.out.println("  Message: " + e.getMessage());
            System.out.println("  Error Code: " + e.getErrorCode());
        }

        // Catch base exception
        System.out.println("\n--- Catch Base Exception (AppException) ---");

        String[] operations = {"find", "validate", "auth", "authz"};

        for (String op : operations) {
            try {
                performOperation(op);
            } catch (AppException e) {
                System.out.println("[" + e.getErrorCode() + "] " +
                    e.getClass().getSimpleName() + ": " + e.getMessage());
            }
        }

        // Check exception type
        System.out.println("\n--- Check Exception Type ---");

        try {
            performOperation("auth");
        } catch (AppException e) {
            if (e instanceof AuthenticationException) {
                System.out.println("Authentication failed - redirect to login");
            } else if (e instanceof AuthorizationException) {
                System.out.println("Not authorized - show error page");
            } else if (e instanceof DataException) {
                System.out.println("Data error - retry or notify user");
            }
        }

        System.out.println("\n=== Key Points ===");
        System.out.println("""
            1. Create a base exception for your application
            2. Group related exceptions under category classes
            3. Include common fields in base (like errorCode)
            4. Catch at appropriate level of specificity
            5. Use instanceof to check type when needed
            """);
    }

    static void findProduct(int id) {
        throw new DataNotFoundException("Product", id);
    }

    static void validateProduct(String name, double price) {
        if (name.isBlank()) {
            throw new DataValidationException("Product name is required");
        }
        if (price < 0) {
            throw new DataValidationException("Price cannot be negative");
        }
    }

    static void performOperation(String operation) {
        switch (operation) {
            case "find" -> throw new DataNotFoundException("User", 1);
            case "validate" -> throw new DataValidationException("Invalid data");
            case "auth" -> throw new AuthenticationException("Invalid credentials");
            case "authz" -> throw new AuthorizationException("admin", "DELETE");
        }
    }
}

// Base exception for the application
abstract class AppException extends RuntimeException {
    private final String errorCode;

    public AppException(String errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }

    public AppException(String errorCode, String message, Throwable cause) {
        super(message, cause);
        this.errorCode = errorCode;
    }

    public String getErrorCode() {
        return errorCode;
    }
}

// Category: Data-related exceptions
abstract class DataException extends AppException {
    public DataException(String errorCode, String message) {
        super(errorCode, message);
    }
}

// Specific: Entity not found
class DataNotFoundException extends DataException {
    private final String entityType;
    private final Object entityId;

    public DataNotFoundException(String entityType, Object entityId) {
        super("NOT_FOUND", entityType + " not found with id: " + entityId);
        this.entityType = entityType;
        this.entityId = entityId;
    }

    public String getEntityType() { return entityType; }
    public Object getEntityId() { return entityId; }
}

// Specific: Validation error
class DataValidationException extends DataException {
    public DataValidationException(String message) {
        super("VALIDATION_ERROR", message);
    }
}

// Category: Service-related exceptions
abstract class ServiceException extends AppException {
    public ServiceException(String errorCode, String message) {
        super(errorCode, message);
    }
}

// Specific: Authentication failure
class AuthenticationException extends ServiceException {
    public AuthenticationException(String message) {
        super("AUTH_FAILED", message);
    }
}

// Specific: Authorization failure
class AuthorizationException extends ServiceException {
    private final String requiredRole;
    private final String action;

    public AuthorizationException(String requiredRole, String action) {
        super("ACCESS_DENIED",
            "Access denied: requires " + requiredRole + " role for " + action);
        this.requiredRole = requiredRole;
        this.action = action;
    }

    public String getRequiredRole() { return requiredRole; }
    public String getAction() { return action; }
}

//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//

Base exception class with specialized subclasses. Catch at any level.

Checked custom exception

Create exceptions that must be handled.

CheckedCustom.java
// Creating Checked Custom Exceptions

import java.io.IOException;

public class CheckedCustom {
    public static void main(String[] args) {
        System.out.println("=== Checked Custom Exceptions ===\n");

        // Checked exception requires handling
        System.out.println("--- Checked Exception Requires Handling ---");

        try {
            loadConfiguration("app.properties");
        } catch (ConfigurationException e) {
            System.out.println("Configuration error!");
            System.out.println("  Message: " + e.getMessage());
            System.out.println("  File: " + e.getConfigFile());
            if (e.getCause() != null) {
                System.out.println("  Cause: " + e.getCause().getMessage());
            }
        }

        // Multiple checked exceptions
        System.out.println("\n--- Multiple Checked Exceptions ---");

        try {
            connectToService("db.example.com", 5432);
        } catch (ConnectionException e) {
            System.out.println("Connection failed!");
            System.out.println("  Host: " + e.getHost());
            System.out.println("  Port: " + e.getPort());
        } catch (ConfigurationException e) {
            System.out.println("Configuration error: " + e.getMessage());
        }

        // When to use checked vs unchecked
        System.out.println("\n--- When to Use Checked ---");
        System.out.println("""
            Use CHECKED custom exceptions when:
            • Caller can reasonably recover
            • External resource failure (files, network)
            • Caller should be forced to handle
            • Part of a public API contract

            Use UNCHECKED custom exceptions when:
            • Programming error (invalid arguments)
            • Unrecoverable situation
            • Caller typically cannot handle
            • Would clutter code with try-catch
            """);

        // Demonstrate forced handling
        System.out.println("--- Forced Handling Demo ---");

        processConfiguration();

        System.out.println("\n=== Key Points ===");
        System.out.println("""
            1. Extend Exception (not RuntimeException) for checked
            2. Caller MUST use try-catch or declare throws
            3. Good for recoverable external failures
            4. Forces caller to think about error handling
            5. Can still include additional data fields
            """);
    }

    // Method that throws checked exception
    static void loadConfiguration(String filename) throws ConfigurationException {
        // Simulate file not found
        throw new ConfigurationException(
            "Configuration file not found",
            filename,
            new IOException("File does not exist: " + filename)
        );
    }

    // Method that throws multiple checked exceptions
    static void connectToService(String host, int port)
            throws ConnectionException, ConfigurationException {
        if (host == null || host.isEmpty()) {
            throw new ConfigurationException("Host is required", "connection.properties");
        }
        // Simulate connection failure
        throw new ConnectionException(host, port, "Connection refused");
    }

    static void processConfiguration() {
        // Option 1: Handle with try-catch
        System.out.println("Option 1: Handle with try-catch");
        try {
            loadConfiguration("settings.xml");
        } catch (ConfigurationException e) {
            System.out.println("  Handled: " + e.getMessage());
        }

        // Option 2: Would be to declare 'throws'
        System.out.println("\nOption 2: Propagate with throws");
        System.out.println("  void myMethod() throws ConfigurationException { ... }");
    }
}

// Checked custom exception
class ConfigurationException extends Exception {
    private final String configFile;

    public ConfigurationException(String message, String configFile) {
        super(message);
        this.configFile = configFile;
    }

    public ConfigurationException(String message, String configFile, Throwable cause) {
        super(message, cause);
        this.configFile = configFile;
    }

    public String getConfigFile() {
        return configFile;
    }
}

// Another checked custom exception
class ConnectionException extends Exception {
    private final String host;
    private final int port;

    public ConnectionException(String host, int port, String message) {
        super(message);
        this.host = host;
        this.port = port;
    }

    public ConnectionException(String host, int port, String message, Throwable cause) {
        super(message, cause);
        this.host = host;
        this.port = port;
    }

    public String getHost() { return host; }
    public int getPort() { return port; }

    public String getConnectionString() {
        return host + ":" + port;
    }
}

//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//

Extend Exception for checked. Use for recoverable errors.

Best practices

Design exceptions effectively.

BestPractices.java
// Exception Design Best Practices

public class BestPractices {
    public static void main(String[] args) {
        System.out.println("=== Exception Design Best Practices ===\n");

        // 1. Provide multiple constructors
        System.out.println("--- 1. Multiple Constructors ---");

        try {
            throw new WellDesignedException("Simple message");
        } catch (WellDesignedException e) {
            System.out.println("Basic: " + e.getMessage());
        }

        try {
            Exception cause = new Exception("Original error");
            throw new WellDesignedException("With cause", cause);
        } catch (WellDesignedException e) {
            System.out.println("With cause: " + e.getMessage());
            System.out.println("  Cause: " + e.getCause().getMessage());
        }

        // 2. Include context data
        System.out.println("\n--- 2. Include Context Data ---");

        try {
            throw new OrderException("ORD-123", "Validation failed", OrderException.Stage.VALIDATION);
        } catch (OrderException e) {
            System.out.println("Order error: " + e.getMessage());
            System.out.println("  Order ID: " + e.getOrderId());
            System.out.println("  Stage: " + e.getStage());
        }

        // 3. Use meaningful names
        System.out.println("\n--- 3. Use Meaningful Names ---");
        System.out.println("""
            Good names (clear what went wrong):
            ✓ InsufficientFundsException
            ✓ UserNotFoundException
            ✓ InvalidEmailFormatException
            ✓ OrderAlreadyShippedException

            Bad names (vague or generic):
            ✗ BadDataException
            ✗ MyException
            ✗ Error1
            ✗ ProcessingException
            """);

        // 4. Document your exceptions
        System.out.println("--- 4. Document with @throws ---");
        System.out.println("""
            /**
             * Processes the order.
             * @param orderId the order to process
             * @throws OrderNotFoundException if order doesn't exist
             * @throws OrderAlreadyProcessedException if already processed
             */
            void processOrder(String orderId) { ... }
            """);

        // 5. Preserve the cause chain
        System.out.println("--- 5. Preserve Cause Chain ---");

        try {
            simulateDatabaseError();
        } catch (DataAccessException e) {
            System.out.println("Exception chain:");
            Throwable current = e;
            int level = 0;
            while (current != null) {
                System.out.println("  " + "  ".repeat(level) +
                    current.getClass().getSimpleName() + ": " + current.getMessage());
                current = current.getCause();
                level++;
            }
        }

        // 6. Consider immutability
        System.out.println("\n--- 6. Make Fields Final ---");
        System.out.println("""
            // Good: final fields
            class MyException extends RuntimeException {
                private final String code;  // Cannot change
                private final int value;    // Cannot change
            }

            // Avoid: mutable state in exceptions
            """);

        System.out.println("\n=== Summary ===");
        System.out.println("""
            ✓ Provide multiple constructors
            ✓ Include relevant context data
            ✓ Use clear, descriptive names
            ✓ Document in Javadoc with @throws
            ✓ Always preserve cause chain
            ✓ Make exception fields final
            ✓ Follow naming convention (*Exception)
            """);
    }

    static void simulateDatabaseError() {
        try {
            throw new RuntimeException("Connection timeout");
        } catch (RuntimeException e) {
            throw new DataAccessException("Failed to execute query", e);
        }
    }
}

// Well-designed exception with multiple constructors
class WellDesignedException extends RuntimeException {

    // Constructor 1: Message only
    public WellDesignedException(String message) {
        super(message);
    }

    // Constructor 2: Message + cause
    public WellDesignedException(String message, Throwable cause) {
        super(message, cause);
    }

    // Constructor 3: Cause only
    public WellDesignedException(Throwable cause) {
        super(cause);
    }
}

// Exception with context data
class OrderException extends RuntimeException {

    // Enum for processing stage
    public enum Stage {
        VALIDATION,
        PAYMENT,
        SHIPPING,
        COMPLETION
    }

    private final String orderId;
    private final Stage stage;

    public OrderException(String orderId, String message, Stage stage) {
        super(String.format("[%s] %s (stage: %s)", orderId, message, stage));
        this.orderId = orderId;
        this.stage = stage;
    }

    public OrderException(String orderId, String message, Stage stage, Throwable cause) {
        super(String.format("[%s] %s (stage: %s)", orderId, message, stage), cause);
        this.orderId = orderId;
        this.stage = stage;
    }

    public String getOrderId() { return orderId; }
    public Stage getStage() { return stage; }
}

// Exception that preserves cause chain
class DataAccessException extends RuntimeException {

    public DataAccessException(String message) {
        super(message);
    }

    public DataAccessException(String message, Throwable cause) {
        super(message, cause);
    }
}

//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//

Include cause, make immutable, provide useful messages.

Exercise: Practical.java

Build a banking system with domain-specific exceptions