Handle errors gracefully — checked vs unchecked, try-with-resources, custom exceptions.
Published September 21, 2026
Things go wrong while programs run: a file is missing, the network drops, input is malformed, a bug dereferences null. Java reports these situations by throwing an exception, an object describing what went wrong. The exception unwinds the call stack until some code catches it and decides what to do, or, if nothing does, the thread ends and prints a stack trace.
Good exception handling keeps error handling separate from the normal path of the code, sends failures to the place that can actually deal with them, and never hides problems.
Throwable
├── Error serious JVM problems: OutOfMemoryError, StackOverflowError — don't catch
└── Exception
├── (checked exceptions) IOException, SQLException, TimeoutException …
└── RuntimeException (unchecked) NullPointerException, IllegalArgumentException,
IllegalStateException, IndexOutOfBoundsException …
Exception but not RuntimeException) must be caught or declared with throws. The compiler enforces this. They represent conditions a caller might reasonably handle: a file that doesn't exist, a network timeout.RuntimeException and its subclasses) need no declaration. They usually indicate programming errors (null references, invalid arguments, calling methods in the wrong state) or failures the immediate caller can't do anything about.String read(Path p) throws IOException { // checked: callers must decide what to do
return Files.readString(p);
}
void setAge(int age) {
if (age < 0) throw new IllegalArgumentException("age must be ≥ 0, was " + age); // unchecked
}
Modern Java code, and frameworks like Spring, lean heavily on unchecked exceptions. Checked exceptions force every layer to mention failures it can't handle, and they don't work well with lambdas and streams. Spring, for example, translates checked SQLExceptions into its unchecked DataAccessException hierarchy.
try {
Order order = repository.load(id);
process(order);
} catch (OrderNotFoundException e) { // most specific first
log.warn("Order {} not found", id);
return Optional.empty();
} catch (IOException | TimeoutException e) { // multi-catch for identical handling (Java 7)
throw new ServiceUnavailableException("Storage unavailable", e); // wrap, keeping the cause
} finally {
metrics.recordAttempt(); // runs whether or not an exception occurred
}
finally always runs, even after return or an exception, which makes it good for cleanup. Never return from finally: it silently discards any exception in flight.Files, database connections, streams and locks must be released even when errors happen. Any class implementing AutoCloseable can be used in try-with-resources (Java 7+). It's closed automatically, in reverse order of opening:
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement("SELECT name FROM users WHERE id = ?")) {
ps.setLong(1, id);
try (ResultSet rs = ps.executeQuery()) {
return rs.next() ? rs.getString(1) : null;
}
} // ps then conn are closed here — even if an exception is thrown
If both the body and close() throw, the body's exception propagates and the close failure is attached as a suppressed exception (e.getSuppressed()) instead of replacing it. That's a detail hand-written finally blocks usually get wrong. Always prefer try-with-resources over manual finally { x.close(); }.
Create your own exception types when callers need to react to a specific failure differently:
public class InsufficientFundsException extends RuntimeException {
private final String accountId;
private final long shortfall;
public InsufficientFundsException(String accountId, long shortfall) {
super("Account " + accountId + " is short by " + shortfall);
this.accountId = accountId;
this.shortfall = shortfall;
}
public String accountId() { return accountId; }
public long shortfall() { return shortfall; }
}
RuntimeException unless you deliberately want to force callers to handle it.PaymentException with a few subclasses) is easier to work with.When translating a low-level exception into a higher-level one, always pass the original as the cause:
try {
return objectMapper.readValue(json, Order.class);
} catch (JsonProcessingException e) {
throw new InvalidOrderPayloadException("Could not parse order JSON", e); // `e` becomes the cause
}
The stack trace then shows both, with "Caused by: …", so the root cause isn't lost. Dropping the cause (throw new X("failed")) is one of the most damaging habits in production debugging.
catch block makes failures invisible. At a minimum log it, and usually rethrow or translate it.catch (Exception e) also catches bugs such as NullPointerException that should crash loudly. Broad catches belong only at the top level (the thread entry point, a request handler) as a safety net.map.containsKey(k) instead of catching an exception from a lookup, and return Optional for "not found" where that's an expected outcome.Objects.requireNonNull(x, "x")) and include the offending values in the message.InterruptedException and can't propagate it, call Thread.currentThread().interrupt(), so the thread's cancellation status isn't lost.Q: When would you use a checked exception versus an unchecked one? A: Use checked when the caller can reasonably be expected to recover, and you want the compiler to force that decision: a missing file where the user can choose another. Use unchecked for programming errors and failures the immediate caller can't meaningfully handle. Most application code today favours unchecked exceptions with centralized handling.
Q: What's the difference between final, finally and finalize?
A: final is a modifier: it prevents reassignment, overriding or extension. finally is a block that always runs after try/catch. finalize() was a garbage-collection callback, now deprecated for removal because it was unpredictable and unsafe. Use try-with-resources or Cleaner instead.
Q: Does finally run if there's a return in the try block?
A: Yes. The finally block runs before the method actually returns. If finally itself returns or throws, it overrides the original return value or exception, which is why returning from finally is considered a bug.
Q: What are suppressed exceptions?
A: In try-with-resources, if the body throws and then closing a resource also throws, the close exception is attached to the primary exception as "suppressed" rather than replacing it. You can see it via getSuppressed() and in the stack trace, so the original failure isn't masked.
Q: Should you catch Throwable or Error?
A: Generally no. Errors like OutOfMemoryError and StackOverflowError indicate the JVM may be in a bad state, and recovery is rarely safe. The narrow exception is top-level infrastructure code that logs something and then exits or rethrows.