Checked versus unchecked, and the long argument about it
Java is the only mainstream language with checked exceptions, and it has been arguing about them since 1997. You need to understand the mechanism because the compiler will not let you past it, and you need to understand the argument because the codebase you join will have taken a side.
The mechanism
Checked exceptions must be either caught or declared. The compiler enforces it:
static void readMissing() {
Files.readAllLines(Path.of("/nope.txt"));
}
error: unreported exception IOException; must be caught or declared to be thrown
Files.readAllLines(Path.of("/nope.txt"));
^
Two ways to satisfy it:
static void readMissing() throws IOException { // declare it — pass the problem up
Files.readAllLines(Path.of("/nope.txt"));
}
static void readMissing() { // or handle it here
try {
Files.readAllLines(Path.of("/nope.txt"));
} catch (IOException e) {
...
}
}
Unchecked exceptions — anything extending RuntimeException — need neither:
Integer.parseInt("abc"); // throws NumberFormatException, compiles fine
The rule is purely structural:
Throwable
├── Error unchecked
└── Exception CHECKED
└── RuntimeException unchecked
Everything under Exception is checked except the RuntimeException
branch. That is the whole definition.
The intent
Checked exceptions were meant to distinguish two kinds of failure.
Recoverable, and the caller should think about it. The file might not exist; the network might be down. These are not bugs — they are situations a correct program must expect. Checked, so the compiler makes you decide.
A bug in the program. Null where there should not be one, an index past the end, an argument that violates a documented rule. Unchecked, because the answer is to fix the code, not to catch it.
By that reading:
| Situation | Kind | Example |
|---|---|---|
| External thing failed | Checked | IOException, SQLException |
| Caller passed rubbish | Unchecked | IllegalArgumentException |
| Object in the wrong state | Unchecked | IllegalStateException |
| Programming mistake | Unchecked | NullPointerException |
| Data did not parse | Either, and it is a judgement |
The argument against
Three complaints, all of them fair.
It produces catch blocks that do nothing. Forced to handle something they
cannot handle, people write:
try {
...
} catch (IOException e) {
e.printStackTrace(); // and carry on as if nothing happened
}
That is worse than not catching it. The program continues in a state the author never considered.
It leaks through abstractions. A UserRepository that throws SQLException
has told every caller it uses a database. Change to a file and every signature
changes with it.
It does not work with lambdas.
paths.stream().map(p -> Files.readString(Path.of(p))).toList();
error: unreported exception IOException; must be caught or declared to be thrown
paths.stream().map(p -> Files.readString(Path.of(p))).toList();
^
Function.apply does not declare any checked exception, so a lambda cannot throw
one. There is no clean fix — you catch inside the lambda and wrap, or you do not
use a stream. This is a genuine design gap, and it is why almost every Java
API written after 2014 uses unchecked exceptions.
Kotlin removed checked exceptions. C# never had them. No language designed since Java has added them.
The argument for
Also fair, and less often made.
They are documentation the compiler enforces. throws IOException on a
signature cannot go stale. A Javadoc @throws on an unchecked exception can, and
does.
They make a failure impossible to overlook. An unchecked exception from a library you did not read is a production incident. A checked one is a compile error on the afternoon you wrote the call.
"People write bad catch blocks" is an argument about people. The empty catch is a choice, and the alternative is not writing anything at all.
What to actually do
The working consensus in modern Java, and what this course follows:
For your own code, throw unchecked exceptions. Extend RuntimeException.
The caller catches what they can handle and lets the rest go up to something that
logs it — a web framework's error handler, or main.
Catch checked exceptions from libraries at a sensible boundary and wrap them in an unchecked one with the cause:
static List<String> loadDeliveries(Path path) {
try {
return Files.readAllLines(path);
} catch (IOException e) {
throw new UncheckedIOException("could not read " + path, e);
}
}
java.io.UncheckedIOException exists in the standard library for exactly this.
For anything else, your own RuntimeException subclass — the next lesson.
"A sensible boundary" means where you can add context. Wrapping immediately
just to avoid the throws gains nothing. Wrapping at the point where you can say
which file, which row, which customer turns a technical failure into something
a support engineer can act on.
Never throws Exception on a signature you own. It says "something might go
wrong" and forces every caller to catch everything, including the bugs.
Never catch and ignore. If you genuinely mean to ignore it, write a comment saying why — and that comment is what a reviewer will check.
throws and overriding
An override may throw fewer or narrower checked exceptions than the method it overrides, never more. That follows from substitutability: code written against the parent must still be correct with the child.
This is why an interface method that declares no checked exception can never be implemented by something that throws one — and it is the same rule that makes lambdas incompatible with checked exceptions.
Check your work
What is the structural rule? Everything under Exception is checked except
the RuntimeException branch. Error is unchecked.
What does the compiler do about a checked exception? Requires you to catch it
or declare it with throws. The error is unreported exception X; must be caught
or declared to be thrown.
What was the intended distinction? Checked for recoverable external failures the caller should consider; unchecked for programming mistakes.
Name the three complaints. Empty catch blocks written to satisfy the compiler; leaking implementation details through signatures; incompatibility with lambdas, because functional interfaces declare no checked exceptions.
What is the argument in favour? They are documentation the compiler keeps honest, and they surface a failure at compile time rather than in production.
What should you throw from your own code? Unchecked — a RuntimeException
subclass. Catch library checked exceptions at a boundary and wrap with the cause.
What does "a sensible boundary" mean? The point where you can add context — which file, which row, which customer. Wrapping without adding context gains nothing.
Why can an override not add checked exceptions? Callers written against the parent must remain correct. An override may throw fewer or narrower, never more.
Practice 2, the lambda. paths.stream().map(p -> Files.readString(...))
gives unreported exception IOException; must be caught or declared to be
thrown, pointing at the call inside the lambda. The two workarounds are catching
inside the lambda and wrapping, or extracting a helper method that does the
wrapping and using a method reference. Neither is pretty, and that is the honest
state of the language.
Practice 4, the wrapping boundary. Wrapping immediately inside
readAllLines adds nothing but a new type name. Wrapping in loadDeliveries,
where the path is known, produces could not read /data/deliveries.csv with the
NoSuchFileException as the cause — a message support can act on without
reading the code.
Practice
-
Meet the compiler error. Call
Files.readAllLineswithout handling anything. Read the message. Then satisfy it both ways —throwsandtry/catch— and say which you would choose for a method called frommain. -
Hit the lambda wall. Put
Files.readStringinside amap. Read the error. Then fix it twice: once by catching inside the lambda, once by extracting a helper method. Decide which you would put in a pull request. -
Write the empty catch, then delete it. Catch an
IOException, callprintStackTrace(), and carry on. Then make the file genuinely missing and look at what your program does next. Write one sentence about whether the program is still correct. -
Wrap at the right boundary. Write
loadDeliveries(Path)that wrapsIOExceptionin an unchecked exception naming the path. Compare its message with one wrapped immediately inside the lowest-level call. -
Break the override rule. Write an interface method with no
throws, then try to implement it with a method that throwsIOException. Read the error and connect it to why lambdas cannot throw checked exceptions. -
Harder — take a side, in writing. Find a real Java library you use and look at whether its exceptions are checked or unchecked. Write two paragraphs on whether you think the choice was right, using its actual signatures as evidence. You will be asked a version of this in an interview, and having looked at real code will put you ahead of a memorised answer.
Next: writing your own exceptions, and making the message worth reading.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship