Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Custom exceptions

There are plenty of built-in exceptions, but they talk about technical details: "division by zero", "no such file". Errors of your domain — "insufficient funds", "out of stock" — are easier to describe with your own classes. Then the exception type alone tells you what happened, and such errors are easy to catch separately.

The simplest exception

A custom exception is an ordinary class that extends Exception or RuntimeException. A constructor that passes the message to the parent through super is enough:

class InsufficientFundsException extends Exception {
    InsufficientFundsException(String message) {
        super(message);
    }
}
 
void main() {
    try {
        throw new InsufficientFundsException("The account has only $40");
    } catch (InsufficientFundsException e) {
        IO.println("Error: " + e.getMessage());
    }
}

Output:

Error: The account has only $40

By convention the class name ends with Exception.

Exception or RuntimeException

The choice of parent decides whether the exception is checked:

  • extends Exception — checked. The compiler forces the calling code to handle it or declare throws. Good for expected situations that need a reaction: not enough money, a username already taken.
  • extends RuntimeException — unchecked. Good for programmer mistakes and broken rules that would be tedious to check at every step.

Modern code more often chooses RuntimeException: there is less boilerplate. But if an error really must be handled, a checked exception won't let anyone forget about it.

An exception in a method

A method that throws a checked exception declares it in throws:

class InsufficientFundsException extends Exception {
    InsufficientFundsException(String message) {
        super(message);
    }
}
 
class Account {
    private int balance = 100;
 
    void withdraw(int amount) throws InsufficientFundsException {
        if (amount > balance) {
            throw new InsufficientFundsException("Need $" + amount + ", available $" + balance);
        }
        balance -= amount;
        IO.println("Withdrew $" + amount + ", left $" + balance);
    }
}
 
void main() {
    Account account = new Account();
    try {
        account.withdraw(30);
        account.withdraw(500);
    } catch (InsufficientFundsException e) {
        IO.println("Declined: " + e.getMessage());
    }
}

Output:

Withdrew $30, left $70
Declined: Need $500, available $70

Extra fields

An exception is an ordinary class, so it can hold data about the error. The handler then doesn't have to parse the message text:

class OutOfStockException extends RuntimeException {
    private final String product;
 
    OutOfStockException(String product) {
        super("Out of stock: " + product);
        this.product = product;
    }
 
    String getProduct() {
        return product;
    }
}
 
void main() {
    try {
        throw new OutOfStockException("Coffee");
    } catch (OutOfStockException e) {
        IO.println(e.getMessage());
        IO.println("Suggest a replacement for: " + e.getProduct());
    }
}

Output:

Out of stock: Coffee
Suggest a replacement for: Coffee

The cause of an exception

A custom exception is often thrown in response to another one — for example, to a NumberFormatException while parsing settings. The original exception is passed as the second constructor argument and becomes the cause:

class ConfigException extends RuntimeException {
    ConfigException(String message, Throwable cause) {
        super(message, cause);
    }
}
 
int readPort(String value) {
    try {
        return Integer.parseInt(value);
    } catch (NumberFormatException e) {
        throw new ConfigException("Invalid port: " + value, e);
    }
}
 
void main() {
    try {
        readPort("80a");
    } catch (ConfigException e) {
        IO.println(e.getMessage());
        IO.println("Cause: " + e.getCause().getMessage());
    }
}

Output:

Invalid port: 80a
Cause: For input string: "80a"

Common mistake

If you don't pass the cause, the original error is lost: the stack trace shows only your exception, and figuring out what really broke becomes hard.

class ConfigException extends RuntimeException {
    ConfigException(String message) {
        super(message);
    }
}
 
void main() {
    try {
        Integer.parseInt("80a");
    } catch (NumberFormatException e) {
        throw new ConfigException("Invalid port");
    }
}

Output:

Exception in thread "main" Main$ConfigException: Invalid port
	at Main.main(Main.java:11)

The name Main$ConfigException means the class is declared in the file with main: Java places such classes inside the implicit class Main. More importantly, neither the original value nor the NumberFormatException is visible here. Add a constructor with a Throwable cause parameter to the class and pass e — then the trace shows a Caused by: section with the original error.

Thanks for reading!

Donate to the site — we work hard for you.