Contents

// 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).