Custom Exceptions
Define your own exception classes to make error handling expressive, precise, and domain-specific.
Overview
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.
Completed: 0 / 4 exercises
Knowledge Check
Question 1 of 4