An exception is Java's way of saying "I cannot continue, and here is exactly why". Handling them well is mostly about restraint: catching only what you can actually do something about, and never hiding what you cannot.
try, catch, finally
void parse(String input) {
try {
int value = Integer.parseInt(input);
System.out.println(value * 2);
} catch (NumberFormatException e) {
System.out.println("not a number: " + input);
} finally {
System.out.println("this runs either way");
}
}
The try block holds code that might fail. The catch runs only if the
named exception is thrown. The finally runs whichever happens — including when the
try returns early — which is why it was traditionally used for cleanup.
You can catch several types, most specific first, or combine them:
void handle(String input) {
try {
System.out.println(Integer.parseInt(input.strip()));
} catch (NumberFormatException | NullPointerException e) {
System.out.println("bad input: " + e.getMessage());
}
}
Order matters: a catch for a parent type placed before a child type makes the child
unreachable, and the compiler rejects it.
try-with-resources
Anything holding a file handle, socket or connection must be closed. Doing that in
finally is verbose and easy to get wrong, so do not:
void read(Path path) throws IOException {
try (BufferedReader reader = Files.newBufferedReader(path)) {
System.out.println(reader.readLine());
} // closed automatically, even if an exception was thrown
}
Any resource declared in the parentheses is closed automatically, in reverse order, before any
catch runs. It works for anything implementing AutoCloseable, including
your own classes. Use it for every resource — there is no case where the manual
finally version is better.
Checked versus unchecked
This split is unique to Java and it is the part people argue about.
| Checked | Unchecked | |
|---|---|---|
| Extends | Exception | RuntimeException |
| Compiler | forces you to catch or declare it | ignores it |
| Means | "this can fail for reasons outside your control" | "someone made a programming mistake" |
| Examples | IOException, SQLException | NullPointerException,
IllegalArgumentException |
// Checked: the compiler will not let you ignore this
void mustHandle(Path path) throws IOException { // declare it...
Files.readString(path);
}
void orCatchIt(Path path) {
try { // ...or handle it
Files.readString(path);
} catch (IOException e) {
System.out.println("could not read: " + e.getMessage());
}
}
// Unchecked: no declaration needed, no compiler involvement
void mayThrow(int divisor) {
System.out.println(10 / divisor); // ArithmeticException if 0
}
The practical guidance: throw unchecked exceptions for programming errors — a null argument, an invalid state, a number that should have been positive. Reserve checked exceptions for genuinely recoverable conditions where the caller has a real decision to make.
Throwing your own
The next two snippets are lifted from the console bank app this site uses for examples, so they are real working code rather than an illustration. First, a base class for everything the bank itself can reject:
public class BankException extends RuntimeException {
public BankException(String message) {
super(message);
}
public BankException(String message, Throwable cause) {
// Always pass the cause along. Dropping it is how a stack trace loses the line that
// actually broke.
super(message, cause);
}
}
Then a specific one that carries data, not just a sentence:
public class InsufficientFundsException extends BankException {
private final BigDecimal requested;
private final BigDecimal available;
public InsufficientFundsException(BigDecimal requested, BigDecimal available) {
super("Insufficient funds: you asked for %s but only %s is available."
.formatted(Money.format(requested), Money.format(available)));
this.requested = requested;
this.available = available;
}
public BigDecimal shortfall() {
return requested.subtract(available);
}
}
Two things that make an exception useful, and this class does both. Put the values in the
message — "you asked for $200.00 but only $150.00 is available" ends the conversation;
"insufficient funds" starts one. And carry the data as fields when the caller needs
to act on it rather than just print it, which is what shortfall() is for.
Note the hierarchy. A caller that wants to handle any bank rule catches BankException;
one that specifically cares about a shortfall catches InsufficientFundsException. Both
work, because the second is a subtype of the first.
Before writing a custom exception, check whether a standard one fits.
IllegalArgumentException, IllegalStateException and
UnsupportedOperationException cover most cases and every Java developer already knows
them.
The four ways to handle one badly
1. Swallowing it. The worst thing in this post:
void terrible(String input) {
try {
Integer.parseInt(input);
} catch (NumberFormatException e) {
// nothing here
}
}
The failure happened, the evidence is destroyed, and the program continues with wrong data. An
empty catch is almost never right. If an exception genuinely is expected and ignorable,
say so in a comment — the comment is what distinguishes a decision from an oversight.
2. Catching Exception to be safe. It also catches the bugs you
needed to see, and everything below it in the hierarchy.
3. Losing the cause. When you wrap an exception, pass the original in:
class DataAccessException extends RuntimeException {
DataAccessException(String message, Throwable cause) {
super(message, cause); // keep the original
}
}
class Repo {
void load(Path path) {
try {
Files.readString(path);
} catch (IOException e) {
// throw new DataAccessException("load failed"); // stack trace lost
throw new DataAccessException("load failed for " + path, e); // "Caused by:" preserved
}
}
}
Dropping the cause is how you end up with a stack trace that points at your wrapper and says nothing about the actual failure. Debugging shows what "Caused by" gives you.
4. Using exceptions for control flow. They are expensive — building a stack trace
is real work — and they hide the logic. If a condition is normal, test for it with an
if.
Where to catch
The most useful rule in the post: catch an exception only where you can do something about it. Logging and rethrowing at every level produces the same error five times in the log and helps nobody.
In practice that means most methods declare or propagate, and one place near the top — a request
handler, a job runner, main — catches, records it once with full detail, and turns it
into whatever the caller should see.
class Runner {
void run() {
try {
doWork();
} catch (Exception e) { // the ONE place this is reasonable
System.err.println("job failed: " + e.getMessage());
e.printStackTrace();
}
}
void doWork() { }
}
Next
That completes the fundamentals. The next section covers the functional style Java 8 introduced, starting with Lambda Expressions.