Exception Handling
BeginnerHandle errors gracefully with try/catch/finally, distinguish checked from unchecked exceptions, and write custom exception classes.
Overview
Exceptions are Java's mechanism for signalling and handling error conditions. The exception hierarchy starts at Throwable, splits into Error (JVM-level, don't catch) and Exception (application-level). Checked exceptions must be declared or handled at compile time; unchecked exceptions (RuntimeException and subclasses) do not. Java 7 introduced multi-catch and try-with-resources — both reduce boilerplate significantly. Good exception design means throwing specific exceptions, never swallowing them silently, and always cleaning up resources.
try / catch / finally & Exception Hierarchy
The exception hierarchy: Throwable ├── Error (OutOfMemoryError, StackOverflowError) — never catch these └── Exception ├── Checked exceptions (IOException, SQLException) — must handle or declare └── RuntimeException (unchecked) ├── NullPointerException ├── ArrayIndexOutOfBoundsException ├── IllegalArgumentException └── ...
finally always runs — even if an exception is thrown or a return statement executes. Use it to release resources. Note: if both the try block and the finally block throw, the try block's exception is lost.
import java.io.IOException;
public class ExceptionDemo {
// Checked exception — must be caught or declared with throws
static String readFile(String path) throws IOException {
if (path == null) throw new IOException("Path cannot be null");
return "file content";
}
public static void main(String[] args) {
// Basic try-catch-finally
try {
String content = readFile(null);
System.out.println(content);
} catch (IOException e) {
System.out.println("IO error: " + e.getMessage()); // IO error: Path cannot be null
} finally {
System.out.println("finally always runs"); // always executes
}
// Multi-catch (Java 7+) — handle multiple types in one block
try {
String s = null;
int[] arr = new int[3];
s.length(); // NullPointerException
arr[5] = 1; // ArrayIndexOutOfBoundsException
} catch (NullPointerException | ArrayIndexOutOfBoundsException e) {
System.out.println("Caught: " + e.getClass().getSimpleName());
}
// Exception chaining — wrap root cause
try {
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
throw new RuntimeException("Calculation failed", e); // wraps original
}
} catch (RuntimeException e) {
System.out.println(e.getMessage()); // Calculation failed
System.out.println(e.getCause().getMessage()); // / by zero
}
}
}try-with-resources & Custom Exceptions
try-with-resources (Java 7+) automatically closes any AutoCloseable resource at the end of the block — even if an exception occurs. This replaces verbose null-check + finally close patterns. Multiple resources are allowed, closed in reverse order of declaration.
Custom exception classes: extend RuntimeException for unchecked (callers aren't forced to handle), or extend Exception for checked. Always provide constructors that accept a message and a cause (for exception chaining).
import java.io.*;
// Custom exceptions
public class InsufficientFundsException extends RuntimeException {
private final double shortage;
public InsufficientFundsException(double shortage) {
super("Insufficient funds. Short by: $" + shortage);
this.shortage = shortage;
}
public InsufficientFundsException(double shortage, Throwable cause) {
super("Insufficient funds. Short by: $" + shortage, cause);
this.shortage = shortage;
}
public double getShortage() { return shortage; }
}
public class ResourceDemo {
// Simulated AutoCloseable resource
static class DbConnection implements AutoCloseable {
public DbConnection(String url) { System.out.println("Opened: " + url); }
public String query(String sql) { return "result"; }
@Override public void close() { System.out.println("Connection closed"); }
}
public static void main(String[] args) {
// try-with-resources — close() called automatically
try (DbConnection conn = new DbConnection("jdbc:localhost/db")) {
String result = conn.query("SELECT 1");
System.out.println(result);
} // close() runs here, even if exception was thrown
// Multiple resources — closed in REVERSE declaration order
try (var in = new BufferedReader(new StringReader("line1
line2"));
var out = new StringWriter()) {
String line;
while ((line = in.readLine()) != null) out.write(line + " ");
System.out.println(out.toString().trim()); // line1 line2
} catch (IOException e) {
e.printStackTrace();
}
// Custom exception
try {
double balance = 100, withdrawal = 150;
if (withdrawal > balance)
throw new InsufficientFundsException(withdrawal - balance);
} catch (InsufficientFundsException e) {
System.out.println(e.getMessage()); // Insufficient funds. Short by: $50.0
System.out.println(e.getShortage()); // 50.0
}
}
}Exception Handling Best Practices
Common mistakes and how to fix them:
Never swallow exceptions — empty catch blocks hide bugs. At minimum, log the exception. Catch specific types — catch (Exception e) hides NullPointerException, bugs, etc. Catch the narrowest applicable type. Don't use exceptions for flow control — exceptions are expensive (stack trace capture). Use if/else for expected conditions. Always wrap causes — when rethrowing, include the original exception as the cause so the root cause is not lost. Clean up resources — use try-with-resources, not finally blocks.
import java.util.logging.Logger;
public class BestPractices {
private static final Logger log = Logger.getLogger(BestPractices.class.getName());
// BAD: swallowing exception
static int parseBad(String s) {
try {
return Integer.parseInt(s);
} catch (NumberFormatException e) {
// silent — caller never knows it failed
return 0;
}
}
// GOOD: translate to domain exception with cause
static int parseGood(String s) {
try {
return Integer.parseInt(s);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid number: '" + s + "'", e);
}
}
// GOOD: use Optional for "not found" — not an exception
static java.util.Optional<Integer> tryParse(String s) {
try {
return java.util.Optional.of(Integer.parseInt(s));
} catch (NumberFormatException e) {
return java.util.Optional.empty();
}
}
public static void main(String[] args) {
System.out.println(parseBad("abc")); // 0 — no indication of error!
try {
parseGood("abc");
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage()); // Invalid number: 'abc'
System.out.println(e.getCause().getClass().getSimpleName()); // NumberFormatException
}
tryParse("42").ifPresent(n -> System.out.println("Parsed: " + n));
tryParse("x").ifPresentOrElse(n -> {}, () -> System.out.println("Not a number"));
}
}Key Points to Remember
- Checked exceptions (extend Exception) must be caught or declared; unchecked (extend RuntimeException) do not
- Never catch Error — JVM errors like OutOfMemoryError are unrecoverable
- finally always runs; try-with-resources (Java 7+) is the modern way to close AutoCloseable resources
- Always chain exceptions with cause: throw new MyException("msg", originalException)
- Never swallow exceptions with an empty catch block — at minimum log them
- Catch the narrowest applicable exception type; avoid catching bare Exception or Throwable
Practice Exception Handling in the Playground
Run and modify code directly in your browser - no setup needed.
Interview Questions
Sign in to ask AriaWhat is the difference between checked and unchecked exceptions?
What is try-with-resources and what interface must a resource implement?
Does finally always run? Give a case where it does not.
How do you create a custom exception class?
What is exception chaining and why is it important?
Ask Aria about Exception Handling
Your personal AI tutor — ask anything about this concept