⚡IntermediateAdvanced Exception Handling

Custom Exceptions

Define your own exception classes to make error handling expressive, precise, and domain-specific.

Overview

Custom exceptions make your code self-documenting. Instead of throwing a generic RuntimeException("invalid email"), you throw an InvalidEmailException. Callers know exactly what can go wrong. To create a custom exception, extend Exception (checked) or RuntimeException (unchecked). You typically provide constructors that pass a message and optionally a cause to the superclass. Checked exceptions force callers to handle or declare them. Unchecked exceptions (extending RuntimeException) do not. Most modern Java code prefers unchecked exceptions to avoid cluttering method signatures.

Key Points

  • •Extend Exception for checked exceptions; extend RuntimeException for unchecked
  • •Always call super(message) or super(message, cause) in your constructor
  • •The cause (Throwable) lets you wrap a lower-level exception without losing context
  • •Exception hierarchy: Throwable -> Error | Exception -> RuntimeException -> your class
  • •Checked exceptions: compiler forces you to catch or declare with throws
  • •Unchecked exceptions: no compiler enforcement, suitable for programming errors
  • •Create a hierarchy of domain exceptions (e.g., AppException -> DatabaseException, ValidationException)

Examples

Simple custom exception

public class Main {
    static class InsufficientFundsException extends RuntimeException {
        private final double amount;
        private final double balance;

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

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

    static class BankAccount {
        private double balance;

        BankAccount(double balance) { this.balance = balance; }

        void withdraw(double amount) {
            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.0);
        try {
            account.withdraw(150.0);
        } catch (InsufficientFundsException e) {
            System.out.println(e.getMessage());
            System.out.printf("You tried to withdraw: %.2f%n", e.getAmount());
            System.out.printf("Available balance: %.2f%n", e.getBalance());
        }
    }
}

The custom exception carries extra data (amount, balance) that generic exceptions cannot. Callers can extract this information for logging or user-friendly messages.

Checked exception with cause

public class Main {
    static class ConfigLoadException extends Exception {
        ConfigLoadException(String filename, Throwable cause) {
            super("Failed to load configuration file: " + filename, cause);
        }
    }

    static void loadConfig(String filename) throws ConfigLoadException {
        try {
            // Simulate a low-level I/O error
            if (filename.equals("missing.cfg")) {
                throw new java.io.IOException("File not found: " + filename);
            }
            System.out.println("Config loaded from " + filename);
        } catch (java.io.IOException e) {
            // Wrap in domain exception to preserve context
            throw new ConfigLoadException(filename, e);
        }
    }

    public static void main(String[] args) {
        try {
            loadConfig("missing.cfg");
        } catch (ConfigLoadException e) {
            System.out.println(e.getMessage());
            System.out.println("Caused by: " + e.getCause().getMessage());
        }
    }
}

Wrapping a low-level exception in a domain-specific one preserves the original cause (getCause()) while presenting a cleaner API. Never swallow the cause by not passing it to super.

Exception hierarchy

public class Main {
    static class AppException extends RuntimeException {
        AppException(String message) { super(message); }
        AppException(String message, Throwable cause) { super(message, cause); }
    }

    static class ValidationException extends AppException {
        private final String field;
        ValidationException(String field, String message) {
            super(field + ": " + message);
            this.field = field;
        }
        String getField() { return field; }
    }

    static class NotFoundException extends AppException {
        NotFoundException(String entity, Object id) {
            super(entity + " not found with id: " + id);
        }
    }

    static String findUser(int id) {
        if (id <= 0) throw new ValidationException("id", "must be positive");
        if (id != 42) throw new NotFoundException("User", id);
        return "Alice";
    }

    public static void main(String[] args) {
        int[] testIds = {-1, 99, 42};
        for (int id : testIds) {
            try {
                System.out.println("User: " + findUser(id));
            } catch (ValidationException e) {
                System.out.println("Validation error on field '" + e.getField() + "': " + e.getMessage());
            } catch (NotFoundException e) {
                System.out.println("Not found: " + e.getMessage());
            } catch (AppException e) {
                System.out.println("App error: " + e.getMessage());
            }
        }
    }
}

An exception hierarchy lets callers catch at the right granularity: catch the specific subclass for precise handling, or catch the base class to handle all app errors together.

Common Mistakes

  • Not calling super(message, cause). If you only call super(message), the original cause is lost. Always pass the cause when wrapping exceptions
  • Creating too many exception classes. Only create a custom exception if it adds value: extra data, a cleaner name, or a meaningful hierarchy
  • Catching a broad exception type (like Exception) and ignoring it. Always log or re-throw to avoid swallowing errors silently

Note

A good rule of thumb: create one base exception per module or domain (e.g., PaymentException), then extend it for specific cases (PaymentDeclinedException, InvalidCardException). This lets callers catch broadly or specifically as needed.

Exercises

Write Java code and click Run to execute it instantly. When your output looks correct, mark as done.

Loading editor…

Completed: 0 / 4 exercises

Knowledge Check

Question 1 of 4

What is the difference between extending Exception and extending RuntimeException?