Exception Handling
Exception handling is Java's structured mechanism for detecting, signaling, and responding to errors and unusual conditions without abandoning normal control flow entirely.
An exception is an event, represented by an object, that disrupts the normal flow of a program's instructions. Java provides a structured mechanism — try, catch, finally, throw, and throws — for detecting these events, handling them where appropriate, and guaranteeing cleanup code runs whether or not an error occurred.
What Is an Exception
When something goes wrong during execution — a file isn't found, an array index is out of bounds, a network call fails — Java represents that problem as an object (an instance of a class descending from Throwable) and "throws" it, immediately halting normal execution and searching up the call stack for code equipped to handle it.
int[] numbers = {1, 2, 3};
try {
System.out.println(numbers[5]); // throws ArrayIndexOutOfBoundsException
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Caught: " + e.getMessage());
}The Throwable Hierarchy
Every exception and error is a subclass of Throwable, which splits into two major branches: Error (serious problems an application generally shouldn't try to catch or recover from) and Exception (problems an application can reasonably anticipate and handle). Exception further splits into checked exceptions and RuntimeException (the root of unchecked exceptions).
Throwable
├── Error (serious JVM/system-level problems — don't catch/recover)
│ ├── OutOfMemoryError
│ └── StackOverflowError
└── Exception
├── IOException (checked)
├── SQLException (checked)
└── RuntimeException (unchecked)
├── NullPointerException
├── ArrayIndexOutOfBoundsException
├── ArithmeticException
└── IllegalArgumentExceptionChecked Exceptions
A checked exception is any subclass of Exception that is NOT a subclass of RuntimeException. Java's compiler enforces, at compile time, that a checked exception must be either caught or declared (via throws) by any method whose code might throw or propagate it — hence "checked."
Files.readString declares throws IOException, an checked exception, so any caller must either catch it (Option 1) or declare it themselves (Option 2) — the compiler enforces this.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
// Option 1: catch it
void readFileSafely(String path) {
try {
String content = Files.readString(Path.of(path)); // may throw IOException
System.out.println(content);
} catch (IOException e) {
System.out.println("Failed to read: " + e.getMessage());
}
}
// Option 2: declare it, pushing the obligation to the caller
void readFileOrThrow(String path) throws IOException {
String content = Files.readString(Path.of(path));
System.out.println(content);
}Unchecked Exceptions
Unchecked exceptions are RuntimeException and all its subclasses (plus Error and its subclasses, though those aren't typically application-recoverable). The compiler does not require them to be caught or declared — they typically represent programming errors (like a null dereference or an out-of-bounds index) that could occur almost anywhere, so mandating handling everywhere would be impractical.
Neither checked nor unchecked exceptions is universally "better" — checked exceptions force callers to consciously address recoverable conditions (like a missing file), while unchecked exceptions avoid cluttering every method signature with exceptions that could occur almost anywhere (like a null argument).
void riskyDivide(int a, int b) {
System.out.println(a / b); // may throw ArithmeticException — no "throws" required
}
riskyDivide(10, 0); // compiles fine; throws ArithmeticException at runtime if not caughtErrors
Error and its subclasses (like OutOfMemoryError and StackOverflowError) represent serious problems that a normal application generally cannot and should not attempt to recover from — they typically indicate the JVM itself is in trouble. Errors are unchecked (not required to be caught or declared) but are conventionally never caught by application code except in specialized diagnostic/logging scenarios.
void recurse() {
recurse(); // eventually throws StackOverflowError
}
// Catching Error is legal but generally discouraged:
try {
recurse();
} catch (StackOverflowError e) {
System.out.println("Stack overflow — the program's stability may already be compromised");
}try / catch
A try block contains code that might throw an exception. One or more catch blocks follow, each handling a specific exception type (or a common supertype). Multiple catch blocks are checked in order, top to bottom, so more specific exception types should be listed before more general ones.
ArrayIndexOutOfBoundsException is a subclass of RuntimeException; listing it first ensures it's matched specifically rather than falling through to the more general RuntimeException catch.
Java 7+ multi-catch (the | syntax) lets one catch block handle several exception types with shared logic, avoiding duplicated catch blocks.
try {
int[] arr = new int[3];
arr[5] = 10; // throws ArrayIndexOutOfBoundsException
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Index problem: " + e.getMessage());
} catch (RuntimeException e) {
System.out.println("Some other runtime problem: " + e.getMessage());
}// Multi-catch: handle several unrelated exception types identically
try {
Integer.parseInt("not a number");
} catch (NumberFormatException | NullPointerException e) {
System.out.println("Bad input: " + e.getMessage());
}finally
A finally block, if present, always runs after the try block finishes, whether it completed normally, threw an exception that was caught, or threw an exception that propagated out uncaught. It's the standard place for cleanup code that absolutely must run.
try-with-resources (using a resource that implements AutoCloseable) is generally preferred over manual finally blocks for closing resources like streams and connections, since it closes them automatically and correctly even when multiple exceptions occur.
void process() {
System.out.println("start");
try {
System.out.println("try");
throw new RuntimeException("boom");
} catch (RuntimeException e) {
System.out.println("catch: " + e.getMessage());
} finally {
System.out.println("finally"); // always runs
}
System.out.println("after");
}
// Output: start, try, catch: boom, finally, afterimport java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
void readFirstLine(String path) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
System.out.println(reader.readLine());
} // reader.close() is called automatically here, even if an exception occurred
}throw
The throw statement explicitly raises an exception, either a newly constructed one or one caught and re-thrown. It immediately transfers control to the nearest enclosing matching catch block (or up the call stack if none exists in the current method).
void setAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative: " + age);
}
// ...
}throws
The throws clause on a method signature declares which checked exceptions the method might propagate to its caller, satisfying the compiler's requirement that checked exceptions be caught or declared. It does not itself throw anything — it's purely a declaration.
import java.io.IOException;
void loadConfig(String path) throws IOException {
// ... code that might throw IOException, not caught here ...
}
void caller() {
try {
loadConfig("config.txt");
} catch (IOException e) {
System.out.println("Could not load config: " + e.getMessage());
}
}Custom Exceptions
Application code commonly defines its own exception classes by extending Exception (for a new checked exception) or RuntimeException (for a new unchecked exception), giving more specific, meaningful error types than the generic ones.
class InsufficientFundsException extends Exception {
public InsufficientFundsException(String message) {
super(message);
}
}
class Account {
private double balance;
void withdraw(double amount) throws InsufficientFundsException {
if (amount > balance) {
throw new InsufficientFundsException("Not enough funds for withdrawal of " + amount);
}
balance -= amount;
}
}Exception Chaining
When handling one exception triggers throwing another, it's good practice to preserve the original exception as the "cause" so debugging retains the full failure chain, rather than losing the original context.
Passing the original exception as the second constructor argument (the cause) means e.getCause() on the outer RuntimeException still returns the original SQLException, preserving the full diagnostic trail.
void loadData() throws RuntimeException {
try {
// ... code that throws a checked SQLException ...
throw new java.sql.SQLException("connection lost");
} catch (java.sql.SQLException e) {
throw new RuntimeException("Failed to load data", e); // e becomes the "cause"
}
}Practical Usage
Typical exception-handling scenarios include validating user input (throwing IllegalArgumentException), handling I/O failures (catching IOException around file/network operations), and using try-with-resources for anything implementing AutoCloseable (streams, database connections, locks).
int parseAge(String input) {
try {
int age = Integer.parseInt(input);
if (age < 0 || age > 150) {
throw new IllegalArgumentException("Age out of realistic range: " + age);
}
return age;
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Age must be a number: " + input, e);
}
}Edge Cases and Common Pitfalls
Catching a broad type like Exception (or worse, Throwable) hides the specific failure and can accidentally swallow unrelated bugs; an empty catch block silently discards errors, making debugging much harder; and a finally block that itself throws or returns can mask an exception from the try block.
// Pitfall: finally with its own return silently discards the try block's exception
int risky() {
try {
throw new RuntimeException("real problem");
} finally {
return 0; // discards the RuntimeException entirely — avoid this pattern
}
}Mental Model
Think of an exception as an alarm that interrupts normal, top-to-bottom execution and travels up the call stack looking for a matching catch block, like an elevator that stops climbing the moment it finds a floor whose door is marked with the right label. If no floor along the way matches, the alarm reaches the very top (the JVM itself), which prints a stack trace and terminates the thread. finally is a promise that a specific piece of cleanup code rings regardless of which floor the alarm stops at, or even if it never stops at all.
Quick Reference
try {
// risky code
} catch (SpecificException e) {
// handle specific case (list most specific catch types first)
} catch (GeneralException e) {
// handle broader case
} finally {
// always runs — cleanup
}
throw new SomeException("message"); // raise an exception
void method() throws CheckedException { } // declare a checked exception may propagate
Checked exception: Exception, not RuntimeException — must be caught or declared
Unchecked exception: RuntimeException and subclasses — optional to catch/declare
Error: serious JVM/system problem — generally don't catchKey Takeaways
Every exception and error is a Throwable; Exception (recoverable) and Error (generally not recoverable) are its two major branches.
Checked exceptions (Exception minus RuntimeException) must be caught or declared with throws, enforced at compile time; unchecked exceptions (RuntimeException and its subclasses) are not required to be.
try wraps risky code; catch handles a specific matched exception type; finally always runs, whether or not an exception occurred, and is the standard place for cleanup — though try-with-resources is generally preferred for closing AutoCloseable resources.
throw raises an exception; throws on a method signature only declares that a checked exception might propagate — it does not throw anything itself.
Neither checked nor unchecked exceptions is categorically superior; each fits different situations, and overly broad catches or swallowed exceptions are common pitfalls to avoid regardless of which kind is involved.
try { riskyOperation(); } catch (Exception e) { // empty — silently swallows every possible problem } try { riskyOperation(); } catch (SpecificException e) { log.warn("Risky operation failed", e); // handle or rethrow as appropriate }
Catching a broad type with an empty body silently discards errors (including bugs unrelated to what you meant to handle), making failures invisible and much harder to diagnose; catch the most specific type you can, and always do something meaningful with it.
int risky() { try { throw new RuntimeException("real problem"); } finally { return 0; // silently discards the exception above } } int risky() { try { throw new RuntimeException("real problem"); } finally { System.out.println("cleanup"); // no return here — exception propagates normally } }
A return (or another exception) inside finally silently discards whatever exception or return value the try block was producing; avoid returning from a finally block.
void loadConfig(String path) { Files.readString(Path.of(path)); // does not compile: unhandled checked IOException } void loadConfig(String path) throws IOException { Files.readString(Path.of(path)); // declared, pushing the obligation to the caller }
A checked exception must be caught or declared; forgetting to do either is a compile-time error, not something that can be silently ignored the way an unchecked exception can.
try { doWork(); } catch (RuntimeException e) { // handles too broadly — order matters } catch (NullPointerException e) { // unreachable! NullPointerException is a RuntimeException, so it's already caught above } try { doWork(); } catch (NullPointerException e) { // specific case handled first } catch (RuntimeException e) { // broader fallback }
Catch blocks are checked in order; a more general exception type listed before a more specific subtype makes the specific catch unreachable, which is actually a compile-time error in Java.
What are the two major branches under Throwable, and how do they differ in intent?
Error and Exception. Error represents serious, generally unrecoverable JVM/system-level problems (like OutOfMemoryError) that applications typically shouldn't try to catch. Exception represents conditions an application can reasonably anticipate and handle, further split into checked exceptions and RuntimeException (unchecked).
What is the difference between a checked and an unchecked exception, in terms of compiler enforcement?
A checked exception (Exception minus RuntimeException) must be either caught or declared with throws — the compiler enforces this at compile time. An unchecked exception (RuntimeException and its subclasses, plus Error) has no such requirement.
What does the finally block guarantee, and why is it used for cleanup code?
A finally block always executes after the try block, regardless of whether the try completed normally, threw a caught exception, or threw an exception that propagates out uncaught — making it the reliable place to release resources or run mandatory cleanup logic.
What is the difference between throw and throws?
throw is a statement that actually raises a specific exception instance at runtime. throws is a declaration on a method signature stating that a checked exception might propagate out of that method; it doesn't throw anything by itself.
Why must catch blocks for related exception types be ordered from most specific to most general?
Catch blocks are evaluated top to bottom, and the first matching type wins; if a more general supertype is listed before a specific subtype, the subtype's catch block becomes unreachable, which Java actually treats as a compile-time error.
Interview: Why does Java distinguish checked from unchecked exceptions instead of treating all exceptions the same way?
Checked exceptions represent conditions a caller can reasonably be expected to anticipate and recover from (like a missing file), so the compiler forces conscious handling. Unchecked exceptions typically represent programming errors (like null dereferences) that could occur almost anywhere, so requiring every method to declare them would be impractical and would clutter signatures without adding real safety.
Interview: Is it accurate to say checked exceptions are always the 'better' design choice for custom exceptions? Why or why not?
No — neither is universally better. Checked exceptions force callers to explicitly address a recoverable failure but can add ceremony to method signatures and propagate awkwardly through layers that can't meaningfully handle them; unchecked exceptions avoid that ceremony but rely on documentation and convention rather than the compiler to ensure callers are aware of possible failures. The right choice depends on whether callers can reasonably be expected to recover from that specific condition.
Write a method divide(int a, int b) that returns a / b, catching ArithmeticException (from division by zero) and returning 0 in that case instead.
public static int divide(int a, int b) { try { return a / b; } catch (ArithmeticException e) { return 0; } }
Division by zero for integers throws ArithmeticException (unlike floating-point division, which produces Infinity/NaN); catching it and returning 0 handles the error gracefully instead of letting it propagate.
What does this print, in order?
Prints A, B, C, D: rethrown — in that order. The try block prints A then throws; the catch block prints B then throws a new exception; finally still runs and prints C even though the catch block is throwing; the new IllegalStateException then propagates out of demo() and is caught by the outer catch, printing D: rethrown.
Write a custom checked exception InvalidOrderException and a method validateOrder(int quantity) that throws it when quantity is less than or equal to zero.
class InvalidOrderException extends Exception { public InvalidOrderException(String message) { super(message); } } public static void validateOrder(int quantity) throws InvalidOrderException { if (quantity <= 0) { throw new InvalidOrderException("Quantity must be positive, got: " + quantity); } }
InvalidOrderException extends Exception (not RuntimeException), making it a checked exception; validateOrder must declare throws InvalidOrderException since it can throw it, and any caller must catch it or declare it themselves.
What happens when this code runs?
It throws a NullPointerException that is NOT caught (the catch block only matches ArithmeticException), so the exception propagates out of main() uncaught. The JVM prints a stack trace and terminates the program abnormally — "done" is never printed.
void demo() {
try {
System.out.println("A");
throw new RuntimeException("oops");
} catch (RuntimeException e) {
System.out.println("B");
throw new IllegalStateException("rethrown");
} finally {
System.out.println("C");
}
}
try {
demo();
} catch (IllegalStateException e) {
System.out.println("D: " + e.getMessage());
}public static void main(String[] args) {
try {
String s = null;
System.out.println(s.length());
} catch (ArithmeticException e) {
System.out.println("arithmetic");
}
System.out.println("done");
}