Java 41: Exception Hierarchy — Checked vs Unchecked & Custom Exceptions
easy⏱ 5 mincoursejava
The Throwable tree
Throwable has two children: Error (JVM-level, unrecoverable — OutOfMemoryError, StackOverflowError; never catch these) and Exception. Under Exception, RuntimeException and its subtree are unchecked; everything else under Exception (IOException, SQLException) is checked — the compiler refuses to compile unless you try/catch or add throws to the method.
Throwable
├── Error (unchecked, don't catch)
└── Exception
├── RuntimeException (unchecked)
│ ├── NullPointerException
│ └── IllegalArgumentException
└── IOException (checked)
└── FileNotFoundException
Custom exceptions & cause chaining
Extend Exception for a checked domain error, or RuntimeException for an unchecked one. Always offer a (String message, Throwable cause) constructor and call super(message, cause) so the stack trace preserves the original. Re-throwing with a new type while attaching the cause is called exception translation — it keeps the abstraction boundary clean without losing diagnostics.
class OrderException extends Exception {
OrderException(String msg, Throwable cause) { super(msg, cause); }
}
try {
db.save(order);
} catch (SQLException e) {
throw new OrderException("could not persist order", e); // chain the cause
}
finally always runs
finally runs even if try or catch executes return, break, or continue — and even if they throw. Never return from finally: it silently swallows exceptions and overrides the try block's return value, hiding bugs. Use finally only for cleanup; prefer try-with-resources (next lesson) for closing resources.
Model a checked exception with a cause
Build LowLevelError and DomainError classes (DomainError carries a cause). Write loadUser(id) that throws LowLevelError for id<0, caught and re-thrown as DomainError with the cause attached. Also write withFinally() that 'returns' a value from try while a finally block logs — proving cleanup runs. Print the chained cause message.
Catch narrow, not Exception
catch (Exception e) hides programming errors like NPE and is hard to reason about. Catch the most specific type you can act on; let the rest propagate. Multi-catch (catch (IOException | SQLException e)) handles unrelated types in one block when the recovery is identical.
Quiz: checked vs unchecked design
When should a new exception be checked? When the caller can reasonably recover (retry, fall back). Use unchecked (RuntimeException) for programming errors the caller can't fix at runtime (bad arguments, illegal state). Modern style leans toward unchecked to avoid throws clutter — but checked is right for genuinely recoverable I/O failures.