Exceptions

76. Custom Exceptions

Create custom exceptions by extending Exception (checked) or RuntimeException (unchecked); always provide a message constructor and a cause-preserving constructor; keep exception class names clear and domain-specific.

Java 21 (core language)

Explanation

Custom exceptions are worth creating when: standard exceptions don't convey enough domain context, you want callers to be able to catch a specific failure class without catching overly broad types, or you need to attach extra state (error code, resource ID, etc.).

A well-written custom exception provides at least: MyException(String message) and MyException(String message, Throwable cause). The cause constructor enables proper exception chaining โ€” always wrap the original exception as the cause rather than swallowing it. Also provide MyException(Throwable cause) for translation patterns.

Choose checked vs unchecked: checked if callers can and should handle the condition; unchecked (RuntimeException subclass) for programming errors or conditions that propagate through many layers. Effective Java recommends erring on the side of unchecked.

Example

// Custom checked exception with domain context
class InsufficientFundsException extends Exception {
    private final double amount;
    private final double balance;

    public InsufficientFundsException(double amount, double balance) {
        super(String.format("Cannot withdraw %.2f: balance is only %.2f", amount, balance));
        this.amount  = amount;
        this.balance = balance;
    }

    // Cause-preserving constructor
    public InsufficientFundsException(double amount, double balance, Throwable cause) {
        super(String.format("Cannot withdraw %.2f: balance is only %.2f", amount, balance), cause);
        this.amount  = amount;
        this.balance = balance;
    }

    public double getAmount()  { return amount; }
    public double getBalance() { return balance; }
}

// Custom unchecked exception
class ConfigurationException extends RuntimeException {
    private final String key;
    public ConfigurationException(String key) {
        super("Missing required configuration: " + key);
        this.key = key;
    }
    public ConfigurationException(String key, Throwable cause) {
        super("Missing required configuration: " + key, cause);
        this.key = key;
    }
    public String getKey() { return key; }
}

public class CustomExceptions {
    static class BankAccount {
        private double balance;
        BankAccount(double balance) { this.balance = balance; }

        void withdraw(double amount) throws InsufficientFundsException {
            if (amount > balance) throw new InsufficientFundsException(amount, balance);
            balance -= amount;
        }
        double getBalance() { return balance; }
    }

    public static void main(String[] args) {
        BankAccount account = new BankAccount(100.00);
        try {
            account.withdraw(150.00);
        } catch (InsufficientFundsException e) {
            System.out.println("Error: " + e.getMessage());
            System.out.printf("Tried to withdraw: %.2f, balance: %.2f%n",
                e.getAmount(), e.getBalance());
        }

        // Unchecked: no try-catch required
        try {
            throw new ConfigurationException("database.url");
        } catch (ConfigurationException e) {
            System.out.println("Config error: " + e.getMessage());
            System.out.println("Missing key: " + e.getKey());
        }
    }
}

Sources