Contents
- Exception Hierarchy
- try-catch-finally
- Multi-catch and Exception Chaining
- try-with-resources
- Custom Exceptions
- Throwable — root of all error/exception classes
- Error — serious JVM problems (OutOfMemoryError, StackOverflowError) — do not catch
- Exception — base for all application exceptions
- RuntimeException (extends Exception) — unchecked; not required to be declared or caught (NullPointerException, IndexOutOfBoundsException, IllegalArgumentException)
- Checked exceptions — all Exception subclasses that are NOT RuntimeException (IOException, SQLException, ParseException) — must be declared with throws or caught
// Unchecked — no need to declare throws
int[] arr = new int[3];
arr[5] = 1; // throws ArrayIndexOutOfBoundsException at runtime
String s = null;
s.length(); // throws NullPointerException at runtime
// Checked — compiler enforces handling
Files.readString(Path.of("missing.txt")); // IOException — must catch or declare throws
A try block wraps code that may throw. Each catch clause handles a specific exception type — order them from most specific to most general, or the compiler will reject unreachable handlers. The finally block executes unconditionally and is the right place for cleanup when not using try-with-resources:
// Basic try-catch
try {
int result = 10 / 0; // throws ArithmeticException
System.out.println(result);
} catch (ArithmeticException e) {
System.err.println("Division by zero: " + e.getMessage());
}
// Multiple catch blocks — most specific first
try {
String s = null;
System.out.println(s.length()); // NullPointerException
int[] arr = new int[3];
arr[5] = 1; // ArrayIndexOutOfBoundsException
} catch (NullPointerException e) {
System.err.println("Null reference: " + e);
} catch (ArrayIndexOutOfBoundsException e) {
System.err.println("Array bounds: " + e);
} catch (RuntimeException e) {
System.err.println("Some runtime error: " + e); // catches any other RuntimeException
}
// finally — always executes (even if exception thrown or return used)
Connection conn = null;
try {
conn = DriverManager.getConnection(url);
// use connection
} catch (SQLException e) {
System.err.println("DB error: " + e);
} finally {
if (conn != null) {
try { conn.close(); } catch (SQLException ignored) {}
}
// Prefer try-with-resources (see below) over manual finally cleanup
}
// Rethrowing
try {
riskyOperation();
} catch (IOException e) {
log.error("Failed", e);
throw e; // rethrow same exception
}
// Throwing a new exception
try {
int age = Integer.parseInt("abc");
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid age format", e); // chained
}
Never catch Exception or Throwable broadly unless you are a top-level handler (e.g., main() or a framework's request handler). Catching broad exception types hides bugs and makes debugging difficult.
Multi-catch (Java 7+) collapses multiple catch clauses that would share the same handler body into a single clause using |. Exception chaining wraps a lower-level exception inside a domain-specific one so that both the cause and the context are preserved in the stack trace:
// Multi-catch (Java 7+) — handle multiple types the same way
try {
if (flag) throw new IOException("io");
else throw new ParseException("parse", 0);
} catch (IOException | ParseException e) {
// e is effectively final — cannot reassign
System.err.println("Input error: " + e.getMessage());
}
// Exception chaining — preserve the original cause
public User loadUser(String path) throws ServiceException {
try {
String json = Files.readString(Path.of(path));
return parse(json);
} catch (IOException e) {
// Wrap in domain exception, preserving original as cause
throw new ServiceException("Failed to load user from " + path, e);
}
}
// Access the chain
try {
loadUser("users.json");
} catch (ServiceException e) {
System.err.println(e.getMessage());
System.err.println("Caused by: " + e.getCause()); // the original IOException
e.printStackTrace(); // shows full chain
}
// getSuppressed() — exceptions added to another via addSuppressed()
// Used automatically by try-with-resources
try-with-resources (Java 7+) automatically closes resources that implement AutoCloseable. It replaces the verbose finally block and correctly handles exceptions thrown in both the body and the close() call:
// Single resource — automatically closed after try block
try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.err.println("Error: " + e);
}
// reader.close() called automatically — even if exception thrown
// Multiple resources — closed in reverse order of declaration
try (var conn = DriverManager.getConnection(url);
var stmt = conn.createStatement();
var rs = stmt.executeQuery("SELECT * FROM users")) {
while (rs.next()) {
System.out.println(rs.getString("name"));
}
} catch (SQLException e) {
System.err.println("DB error: " + e);
}
// Suppressed exceptions — if both body and close() throw,
// the close() exception is suppressed and attached to the body exception
try (var r = new FaultyResource()) {
throw new RuntimeException("body exception");
// r.close() also throws — that exception is suppressed
} catch (RuntimeException e) {
System.err.println(e.getMessage()); // "body exception"
for (Throwable s : e.getSuppressed()) {
System.err.println("Suppressed: " + s.getMessage()); // "close exception"
}
}
// Effectively-final variables in try-with-resources (Java 9+)
BufferedReader br = new BufferedReader(new FileReader("file.txt"));
try (br) { // no re-declaration needed — br must be effectively final
System.out.println(br.readLine());
}
Custom exceptions convey domain-specific failure conditions. Extend Exception for checked exceptions (the caller must handle or declare them) or RuntimeException for unchecked exceptions. Always provide at least a message constructor and a cause-chaining constructor:
// Checked custom exception — extends Exception
public class InsufficientFundsException extends Exception {
private final double amount;
public InsufficientFundsException(double amount) {
super("Insufficient funds: need " + amount + " more");
this.amount = amount;
}
public InsufficientFundsException(double amount, Throwable cause) {
super("Insufficient funds: need " + amount + " more", cause);
this.amount = amount;
}
public double getAmount() { return amount; }
}
// Unchecked custom exception — extends RuntimeException
public class UserNotFoundException extends RuntimeException {
private final long userId;
public UserNotFoundException(long userId) {
super("User not found: " + userId);
this.userId = userId;
}
public long getUserId() { return userId; }
}
// Using custom exceptions
class BankAccount {
private double balance;
public void withdraw(double amount) throws InsufficientFundsException {
if (amount > balance) {
throw new InsufficientFundsException(amount - balance);
}
balance -= amount;
}
}
// Catching custom exceptions
try {
account.withdraw(500.0);
} catch (InsufficientFundsException e) {
System.err.printf("Need %.2f more%n", e.getAmount());
}
Prefer unchecked (RuntimeException) custom exceptions in application code — they don't force callers to handle them and keep APIs cleaner. Use checked exceptions only when the caller can reasonably recover from the error (e.g., file not found, invalid input).