Exception Handling
Exceptions let a program signal and recover from abnormal conditions without polluting normal control flow with error codes. Java’s exception model is explicit and type-safe: methods declare what can go wrong, and the compiler enforces handling for an important category of failures. Used well, exceptions make failure paths as clear as success paths.
The Exception Hierarchy
Everything throwable descends from Throwable, which splits into Error (JVM-level problems you should not catch) and Exception (recoverable conditions). Exception further splits into checked exceptions and RuntimeException (unchecked).
Throwable
/ \
Error Exception
(unchecked) / \
checked RuntimeException
(unchecked)
e.g. OutOfMemoryError e.g. IOException e.g. NullPointerException
StackOverflowError SQLException IllegalArgumentException
| Category | Compiler enforces? | Examples | When to use |
|---|---|---|---|
| Checked | Yes — must catch or declare | IOException, SQLException | Recoverable, external failures |
Unchecked (RuntimeException) | No | NullPointerException, IllegalArgumentException | Programming errors / bugs |
Error | No (don’t catch) | OutOfMemoryError | Fatal JVM conditions |
Note: Checked exceptions force the caller to acknowledge failure. Unchecked exceptions usually indicate a bug that should be fixed, not handled.
try / catch / finally
public int parse(String s) {
try {
return Integer.parseInt(s);
} catch (NumberFormatException e) {
System.out.println("Not a number: " + s);
return -1;
} finally {
System.out.println("cleanup runs no matter what");
}
}
The finally block executes whether or not an exception is thrown — ideal for releasing resources. It even runs when the try block returns.
Warning: Never
returnfrom afinallyblock. It silently discards any exception or return value from thetryblock, hiding failures.
try-with-resources
For anything implementing AutoCloseable, try-with-resources closes it automatically, in reverse order of declaration, even on exception. This replaces verbose finally cleanup.
import java.io.*;
try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
return reader.readLine();
} catch (IOException e) {
System.out.println("read failed: " + e.getMessage());
return null;
}
// reader.close() is called automatically
This also handles suppressed exceptions: if both the body and close() throw, the body’s exception propagates and the close exception is attached via getSuppressed().
Multi-catch
When several exception types share handling logic, combine them with | instead of duplicating blocks.
try {
riskyOperation();
} catch (IOException | SQLException e) {
log.error("operation failed", e);
throw new ServiceException("could not complete", e);
}
The combined exception variable is implicitly final; you cannot reassign e.
Custom Exceptions
Define your own exceptions to convey domain meaning. Extend RuntimeException for unchecked, Exception for checked. Always preserve the cause.
public class InsufficientFundsException extends RuntimeException {
private final double shortfall;
public InsufficientFundsException(double shortfall, Throwable cause) {
super("Short by $" + shortfall, cause);
this.shortfall = shortfall;
}
public double getShortfall() {
return shortfall;
}
}
throw vs throws
These look similar but do opposite jobs.
throwis a statement that raises an exception instance now.throwsis a clause in a method signature declaring which checked exceptions may escape.
public void withdraw(double amount) throws AccountException { // declares
if (amount <= 0) {
throw new IllegalArgumentException("amount must be positive"); // raises
}
}
Best Practices
- Never swallow exceptions with an empty
catch {}— at minimum log them with full context. - Catch the most specific exception type; avoid catching bare
ExceptionorThrowable. - Always preserve the original cause when wrapping (
new XException(msg, cause)). - Use try-with-resources for every
AutoCloseable. - Throw unchecked exceptions for programming errors; reserve checked exceptions for recoverable conditions.
- Don’t use exceptions for normal control flow — they are expensive and obscure intent.
- Fail fast: validate arguments early and throw
IllegalArgumentException/NullPointerException.
Interview Questions
Q: What is the difference between checked and unchecked exceptions?
Checked exceptions (subclasses of Exception but not RuntimeException) must be caught or declared with throws; the compiler enforces this. Unchecked exceptions (RuntimeException and Error) need no declaration and usually signal bugs or fatal conditions.
Q: What happens if both the try block and finally throw an exception?
The exception from finally replaces the one from try, which is lost. This is why you should avoid throwing or returning from finally. Try-with-resources avoids the problem by suppressing the close exception rather than discarding the original.
Q: Why prefer try-with-resources over a finally block?
It is less error-prone: resources are closed automatically in reverse order, even on exception, and the original exception is preserved while the close exception is attached as suppressed. Manual finally cleanup is verbose and easy to get wrong.
Q: When should you create a custom exception? When existing types don’t convey the domain failure clearly or you need to attach extra data (like a shortfall amount). Custom exceptions improve readability and let callers handle specific conditions distinctly.