Exception Handling
Checked vs unchecked exceptions, try-with-resources, custom exceptions, chaining and best practices for production code.
The exception hierarchy
classDiagram
Throwable <|-- Error
Throwable <|-- Exception
Exception <|-- IOException
Exception <|-- SQLException
Exception <|-- RuntimeException
RuntimeException <|-- IllegalArgumentException
RuntimeException <|-- NullPointerException
RuntimeException <|-- IllegalStateException
Error <|-- OutOfMemoryError
Error <|-- StackOverflowError
class Error {
JVM problems - don't catch
}
class RuntimeException {
unchecked
}
class IOException {
checked
}
| Type | Checked at compile time? | Meaning | Examples |
|---|---|---|---|
Checked (Exception but not RuntimeException) |
Yes — must catch or declare throws |
Recoverable conditions outside the program’s control | IOException, SQLException |
Unchecked (RuntimeException) |
No | Programming errors or violated preconditions | NullPointerException, IllegalArgumentException |
| Error | No | Serious JVM problems you shouldn’t handle | OutOfMemoryError, StackOverflowError |
try / catch / finally
try {
process(order);
} catch (ValidationException | PricingException e) { // multi-catch (Java 7)
log.warn("Rejected order {}: {}", order.id(), e.getMessage());
} catch (RuntimeException e) { // more general types last
log.error("Unexpected failure", e);
throw e;
} finally {
metrics.increment("orders.processed"); // always runs*
}
* finally runs whether the try completes normally, throws, or returns — except if the JVM exits or the thread is killed.
try-with-resources
Any AutoCloseable declared in the try (...) header is closed automatically, in reverse order, even if an exception is thrown. Exceptions thrown by close() are attached as suppressed exceptions rather than hiding the original.
class Resource implements AutoCloseable {
private final String name;
private final boolean failOnClose;
Resource(String name, boolean failOnClose) {
this.name = name; this.failOnClose = failOnClose;
System.out.println("open " + name);
}
void use() { System.out.println("use " + name); throw new IllegalStateException("boom while using " + name); }
@Override public void close() {
System.out.println("close " + name);
if (failOnClose) throw new IllegalArgumentException("close failed for " + name);
}
}
class Main {
public static void main(String[] args) {
try (var db = new Resource("db", false);
var file = new Resource("file", true)) {
file.use();
} catch (IllegalStateException e) {
System.out.println("caught: " + e.getMessage());
for (Throwable s : e.getSuppressed()) System.out.println(" suppressed: " + s.getMessage());
}
}
}Resources are closed before the catch block runs, file before db, and the failure in close() is preserved as a suppressed exception instead of replacing the real error.
Custom exceptions
Create domain-specific exceptions when callers need to react to a specific failure. Extend RuntimeException for most business errors in modern code (Spring translates and handles them centrally).
public class InsufficientFundsException extends RuntimeException {
private final String accountId;
private final BigDecimal shortfall;
public InsufficientFundsException(String accountId, BigDecimal shortfall) {
super("Account %s is short by %s".formatted(accountId, shortfall));
this.accountId = accountId;
this.shortfall = shortfall;
}
public BigDecimal shortfall() { return shortfall; }
}
Exception chaining
When translating a low-level exception into a higher-level one, always pass the cause so the original stack trace isn’t lost:
try {
return objectMapper.readValue(json, Order.class);
} catch (JsonProcessingException e) {
throw new InvalidOrderException("Malformed order payload", e); // ← cause preserved
}
Best practices
- Don’t swallow exceptions. An empty
catch {}hides bugs. At minimum log, rethrow or convert. - Catch the most specific type you can actually handle. Avoid
catch (Exception e)except at boundaries (a top-level handler, a scheduled job loop). - Fail fast — validate arguments early with
Objects.requireNonNullandIllegalArgumentException. - Don’t use exceptions for control flow — they’re slow (stack trace capture) and obscure intent. Return
Optionalor a result type for expected outcomes like “not found”. - Log or rethrow, not both — double logging produces confusing duplicate stack traces.
- Never catch
ThrowableorErrorunless you are writing framework code. - Restore the interrupt flag if you catch
InterruptedExceptionand can’t rethrow it:Thread.currentThread().interrupt().
Helpful NullPointerExceptions
Since Java 14, NPE messages name exactly what was null:
Cannot invoke "String.length()" because the return value of "User.email()" is nullMark it complete to track your progress through Modern Java.