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 $40By 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 declarethrows. 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 $70Extra 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: CoffeeThe 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.