Java
Java Exceptions
›Short versionOpen if you only have a minute.
- An exception is an object a method throws when it cannot finish the job it was asked to do. A catch clause is a caller deciding what to do with that object.
- Checked exceptions extend Exception and are not RuntimeException. The compiler makes every caller catch them or declare them.
- Unchecked exceptions extend RuntimeException. Callers may handle them, and the signature stays silent. That fits a bug, and it also fits a domain failure only a distant boundary can answer.
- The design choice is who must notice. Force the caller when someone nearby can respond. Leave it unchecked when the repair is a code change, or when intermediate layers would only pass the clause along.
- throw sends an object. throws lists checked types on the method. Using throw to end an ordinary loop hides bugs and spends time filling a stack trace.
A method is a promise to do a piece of work and return. An exception is how the method says it could not keep that promise, and how the caller is given the failure as an object with a type, a message, and a trail of calls. Java then makes a sharp design choice about which of those failures the compiler will force the caller to face.
On this page
Checked vs unchecked
Who the compiler forces to notice.
Your own types
Exception vs RuntimeException on purpose.
try / catch
Handle one failure without losing the thread.
Control flow
When throw is not a branch.
When checked fits
- A nearby caller can choose another path, retry, or tell a person.
- IOException, SQLException, and domain types like InsufficientFundsException belong here.
- The signature advertises the failure; empty catch blocks are a smell that the layer cannot decide.
When unchecked fits
- The caller broke the contract (null, bad index, illegal argument).
- Only a distant boundary can respond; middle layers would only re-declare throws.
- Wrap a checked failure once with cause preserved if signatures must stay about success types.
Checked vs unchecked at a glance
| Kind | Extends | Compiler | Typical examples |
|---|---|---|---|
| Checked | Exception (not RuntimeException) | Catch or declare throws on every caller | IOException, SQLException, FileNotFoundException |
| Unchecked | RuntimeException (or Error) | Optional catch; signature may omit throws | NullPointerException, IllegalArgumentException, NumberFormatException |
Read the diagram definition
sequenceDiagram
participant Caller
participant Middle
participant Leaf
Caller->>Middle: call read(path)
Middle->>Leaf: Files.readString(path)
Leaf-->>Middle: throw IOException
Middle-->>Caller: propagates (or wraps)
Caller->>Caller: catch IOException and respondWho is forced to notice
Under Throwable, Error is the runtime's own trouble. Exception is the program's. Inside Exception, is the line the compiler uses. A type under RuntimeException is an . Any other Exception is a .
These two methods show the rule with no extra machinery. Reading a file can fail because of the world outside the program, so read must declare IOException or catch it. Parsing text fails because the text is not an integer. Integer.parseInt throws NumberFormatException, which extends IllegalArgumentException, which extends RuntimeException. The compiler accepts parse with an empty signature.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class TwoFailures {
static String read(Path path) throws IOException {
return Files.readString(path);
}
static int parse(String text) {
return Integer.parseInt(text);
}
}The checked form puts the failure in the contract. A caller of read cannot compile while pretending the file will be there. That is the right pressure when the caller can do something: choose another path, tell a person, retry a network call that is supposed to flake. IOException, SQLException, and FileNotFoundException were designed for that kind of neighbor.
The unchecked form leaves the contract about the success type. parse returns an int. Bad text is possible, and the caller who cares can catch NumberFormatException. Callers who already know the text is an integer are not forced to write a ceremony they will never use. The same shape fits programming mistakes. A null where a value was required, an index past the end, an age the method's contract excludes: the repair is to stop passing that argument. A throws NullPointerException on every method that touches an object would train people to ignore the clause.
A type you choose on purpose
Your own types follow the same split. Extend Exception when a nearby caller should be forced to admit the outcome. Extend RuntimeException when the failure is a broken call, or when only the edge of the program will choose a response and the methods in between should stay quiet.
class InsufficientFundsException extends Exception {
InsufficientFundsException(String message) {
super(message);
}
}
class BankAccount {
private int cents;
BankAccount(int cents) {
this.cents = cents;
}
void withdraw(int amount) throws InsufficientFundsException {
if (amount > cents) {
throw new InsufficientFundsException(
"Balance " + cents + " cents, requested " + amount
);
}
cents -= amount;
}
}withdraw can be called correctly and still fail, because the balance is data, not a bug in the caller. A checked type makes the teller, the ATM, or the transfer method decide what a person sees. Compare a method setAge that rejects a negative number with IllegalArgumentException. The caller broke the contract. Catching that and continuing would paper over a bug. The belongs there so a test or a log sees it. A clause does not, because every legal caller would have to mention a case they are not supposed to produce.
When you wrap, pass the original throwable as the cause. new ConfigLoadException("Could not load " + path, e) keeps the file error under the domain sentence. A wrapper built with only a string keeps the sentence and drops the trail. Propagation, later, is about where that wrap should sit.
Catching one failure
try marks the region that may throw. catch names the type you are willing to handle. If the thrown object is that type or a subtype, that block runs, and the statements after the whole try statement run next, unless the catch block throws again or returns.
public class Divide {
public static void main(String[] args) {
try {
int result = 10 / 0;
System.out.println(result);
} catch (ArithmeticException e) {
System.out.println("Cannot divide by zero: " + e.getMessage());
}
System.out.println("Still running");
}
}The division throws ArithmeticException before println of the result. The catch prints the message / by zero. Then Still running prints, because the exception was handled. A catch of Exception would also match, since ArithmeticException extends RuntimeException, which extends Exception. The narrower type is the one that says what you actually planned for. Several catch clauses, and the order among them, have a page of their own.
An ordinary result is not a throw
throw changes which line runs next, so it can be mistaken for a branch. Some library methods really do report an expected kind of bad input that way. Integer.parseInt throws NumberFormatException when the text is not an integer. Catching that once, where text enters the program, is handling the contract that method published.
Ending a loop with an exception is a different use. The length of an array is an ordinary fact. You already have it. This version treats the end of the array as a failure, and it treats a successful walk as a throw.
int sum = 0;
int i = 0;
try {
while (true) {
sum += values[i++];
}
} catch (ArrayIndexOutOfBoundsException e) {
// hoped this meant "the array ended"
}The catch ends the loop when that exact type is thrown. A bug in the body that throws ArrayIndexOutOfBoundsException for a different reason ends the loop too, and the method looks finished. If values is null, the body throws NullPointerException instead, and this catch does not see it. Creating the exception also fills a stack trace, which walks the frames. On a rare failure that cost is the price of a good log. On every iteration it is the work the loop exists to do.
When the outcome is one the caller expects as often as success, return it. A boolean, a count, an Optional empty of a value. Keep throw for the case where the method cannot finish the job. The loop above is a for loop over values.length, and the sum is the result, not a side effect of a catch block.
parseInt throws when the text is not an integer. You catch NumberFormatException around a single box a person just typed in. Is that using exceptions as control flow?
That catch is handling the failure parseInt chose as its report. The bad text is expected at the edge, and it happens once per submission, so the stack trace is cheap enough. The misuse is a throw you invent to steer a normal path: leaving a loop, choosing a branch you could have written with if, or signaling "not found" on a hot lookup where absence is a routine answer. If absence is routine, return an empty result. If the method truly cannot continue, throw, and catch at the layer that can decide.
Principal-engineer rule
Put a failure in the signature only when the caller can do something useful with it. Use unchecked for contract breaks and for failures that only an outer boundary should answer, and never use throw to steer a loop you could write with a normal condition.
Good default: declare IOException on methods that touch the file system; catch NumberFormatException once at the input edge; extend Exception for recoverable domain outcomes the caller must face, RuntimeException for bugs and for wrap-once propagation.