Java's Exception Hierarchy
The Root: Throwable
java.lang.Throwable is the common superclass of everything that can be thrown with a throw statement. It provides:
getMessage()— the detail message stringgetCause()— the “causing” throwable (for exception chaining)getStackTrace()— an array ofStackTraceElementrepresenting the call stack at the point of throwprintStackTrace()— prints the stack trace toSystem.errgetSuppressed()— suppressed exceptions from try-with-resources
Branch 1: Error
Error and its subclasses represent serious, generally unrecoverable JVM-level problems:
| Error Subclass | Meaning |
|---|---|
OutOfMemoryError | The JVM cannot allocate more heap memory |
StackOverflowError | A thread’s stack has overflowed (usually unbounded recursion) |
InternalError | An unexpected internal error in the JVM itself |
NoClassDefFoundError | A class that was present at compile time is not available at runtime |
AssertionError | An assert statement evaluated to false |
Errors are unchecked — the compiler does not require you to declare or catch them. Application code should generally not catch Error — if the JVM is out of memory, there is little your code can do about it, and attempting complex recovery in that state may make things worse.
Branch 2: Exception
Exception is the class for conditions that application code might reasonably want to handle. It splits into two categories.
Checked Exceptions
Subclasses of Exception that are not subclasses of RuntimeException. The compiler forces every caller to either catch them or declare them in a throws clause.
Checked exceptions are used for recoverable failure conditions — things that can go wrong even in a correctly written program:
| Checked Exception | Meaning |
|---|---|
IOException | General I/O failure |
FileNotFoundException | A file does not exist (subclass of IOException) |
SQLException | Database error |
ParseException | String parsing failure |
ClassNotFoundException | A class name was not found by Class.forName() |
InterruptedException | A thread’s sleep() or wait() was interrupted |
The design philosophy: “this could go wrong, and you need a plan.” The compiler ensures you have one.
Unchecked Exceptions
RuntimeException and its subclasses. No compiler enforcement — the caller is not required to catch or declare them.
Unchecked exceptions are used for programming errors — bugs that should be fixed in the code, not handled at runtime:
| Unchecked Exception | Meaning |
|---|---|
NullPointerException | Dereferencing a null reference |
IllegalArgumentException | A method received an illegal argument |
IndexOutOfBoundsException | Array or list index is out of range |
ConcurrentModificationException | Collection was structurally modified during iteration |
ArithmeticException | Integer division by zero |
ClassCastException | Invalid cast at runtime |
NumberFormatException | Invalid string-to-number conversion |
The design philosophy: “you wrote a bug.” The compiler assumes these should not happen in correctly written code, so it does not burden every caller with handling them. If they do happen, they propagate upward, usually terminating the affected thread and producing a stack trace that helps the developer find the bug.
The Philosophy: Checked versus Unchecked
| Category | When to Use | Compiler Enforcement |
|---|---|---|
| Checked Exception | Recoverable, expected failure (file not found, network timeout) | Caller must catch or declare |
| Unchecked Exception | Programming error (null pointer, invalid argument) | No enforcement |
| Error | JVM failure (out of memory, stack overflow) | No enforcement — generally should not be caught |
The distinction is a matter of fault versus failure: a fault is a defect in the code (unchecked exception), while a failure is an external condition that correct code must cope with (checked exception).
Creating Custom Exceptions
public class InsufficientFundsException extends Exception {
private final double balance;
private final double requested;
public InsufficientFundsException(double balance, double requested) {
super("Insufficient funds: balance=" + balance + ", requested=" + requested);
this.balance = balance;
this.requested = requested;
}
public double getBalance() { return balance; }
public double getRequested() { return requested; }
}
Extend Exception for a checked custom exception, or RuntimeException for an unchecked one. Always provide a meaningful message and, where useful, additional fields that capture the context of the error. Provide constructors that accept a message and optionally a cause (for exception chaining: super(message, cause)).
Exception Chaining
When wrapping a low-level exception in a domain-specific one, pass the original as the cause:
try {
dbConnection.execute(sql);
} catch (SQLException e) {
throw new DataAccessException("Failed to execute query", e);
}
The caller can inspect getCause() to see the original SQLException, maintaining the full diagnostic chain while presenting a clean API.