What is the difference between checked and unchecked exceptions?
Checked exceptions are enforced by the compiler; unchecked ones are not. The interesting question is not the definition but why every major Java framework since 2004 has moved to unchecked exceptions — and lambdas finished the argument in Java 8.
The Answer
Say this in the room. 45 seconds.
- Checked: extends
Exceptionbut notRuntimeException. The compiler forces you to catch it or declarethrows.IOException,SQLException. - Unchecked: extends
RuntimeException. No compiler enforcement.NullPointerException,IllegalArgumentException,IllegalStateException. Erroris a third category —OutOfMemoryError,StackOverflowError. Unchecked, and not yours to catch.- The intended rule: checked for conditions a caller can plausibly recover from, unchecked for programming mistakes.
- The practice: nearly every modern framework uses unchecked exceptions. Spring wraps
SQLExceptioninto uncheckedDataAccessException; JPA and Hibernate throw unchecked. - Java 8 settled it. No standard functional interface declares
throws, so a checked exception cannot escape a lambda — which makes checked exceptions incompatible with streams.
Understand It
The hierarchy, and the one line that defines everything
Throwable
├── Error unchecked — JVM is broken, don't catch
├── Exception CHECKED
│ ├── IOException
│ ├── SQLException
│ └── RuntimeException unchecked — the exception to the rule
│ ├── NullPointerException
│ ├── IllegalArgumentException
│ └── IllegalStateException
└── ...
Everything under Exception is checked except the RuntimeException subtree. That single carve-out is the whole mechanism. There is no keyword and no annotation — checkedness is decided purely by where you sit in that hierarchy, which is why extends RuntimeException is the only decision you make when writing a custom exception.
The argument that was actually lost
The design intent was sound: make the caller acknowledge failures they could handle. Two things went wrong in practice.
It doesn't compose. A method that calls three libraries collects three unrelated checked types. You either declare all of them, and they become part of your signature forever, or you collapse to throws Exception, which discards the information the feature exists to provide. Change a private implementation detail — swap a file for an HTTP call — and a checked exception change ripples through every caller up the stack, even though nothing about the contract changed.
So people silence them. The predictable outcome is the single worst construct in Java:
try {
doSomething();
} catch (IOException e) {
// TODO
}
A feature that was meant to force handling instead trained a generation to write empty catch blocks. C# looked at this and deliberately shipped without checked exceptions. Kotlin, running on the same JVM with the same libraries, has none either.
The frameworks voted the same way. Spring's JdbcTemplate catches SQLException and rethrows unchecked DataAccessException, with an explicit rationale: you can't meaningfully recover from most SQL errors at the call site, so forcing every caller to handle them produces noise, not resilience. Hibernate moved to unchecked in 3.0. JPA is unchecked by specification.
Java 8 ended the debate structurally
This is the part that turns an opinion into a constraint. Look at Function:
@FunctionalInterface
public interface Function<T, R> {
R apply(T t); // no throws clause
}
No throws. So this does not compile:
List<String> paths = List.of("a.txt", "b.txt");
paths.stream()
.map(p -> Files.readString(Path.of(p))) // IOException — won't compile
.toList();
Files.readString throws IOException; Function.apply cannot. Your options are all bad: wrap every lambda body in try/catch that rethrows unchecked, write your own throwing functional interface and adapters for it, or pull a helper method out just to hold the try/catch.
Whatever you pick, the checked exception becomes unchecked at the lambda boundary. Since streams and lambdas are now how Java is written, checked exceptions lost by construction, not by argument. Any API designed after 2014 that wants to be usable from a stream throws unchecked.
The Java 7 change that mattered more than the syntax
try-with-resources gets sold as "closes the resource for you". The important half is what happens when both the body and close() fail. Watch the difference:
System.out.println("--- try-with-resources (Java 7+) ---");
try {
withTryWithResources();
} catch (Exception e) {
System.out.println("caught : " + e.getClass().getSimpleName() + ": " + e.getMessage());
for (Throwable s : e.getSuppressed()) {
System.out.println("suppressed : " + s.getClass().getSimpleName() + ": " + s.getMessage());
}
}
System.out.println("--- hand-written finally (pre-7 style) ---");
try {
withHandWrittenFinally();
} catch (Exception e) {
System.out.println("caught : " + e.getClass().getSimpleName() + ": " + e.getMessage());
System.out.println("suppressed : " + e.getSuppressed().length + " (the real cause is gone)");
}--- try-with-resources (Java 7+) ---
caught : IOException: query failed
suppressed : IllegalStateException: close failed: db
--- hand-written finally (pre-7 style) ---
caught : IllegalStateException: close failed: db
suppressed : 0 (the real cause is gone)Read those two caught lines again. The query failed in both runs. The pre-7 version reports a close failure and the IOException is destroyed — not logged, not chained, gone. You are debugging an incident with the wrong exception in your hands.
That is what suppressed exceptions fixed, and it's the answer to "why should I use try-with-resources when I can write finally?" The resource management is convenience. Not losing the cause is correctness.
finally can still eat your exception
One construct still discards exceptions silently, and the compiler allows it:
System.out.println("returnInsideFinally() = " + returnInsideFinally());
System.out.println("...and the RuntimeException it threw? Gone.");returnInsideFinally() = 42
...and the RuntimeException it threw? Gone.A return inside finally overrides everything in flight — including a pending exception. So does a break, a continue, or throwing a new exception from finally. Never put a control-flow statement in a finally block. Most static analysers flag it; javac only warns with lint enabled.
Helpful NPEs, and the flag that decides them
NullPointerException is the unchecked exception you'll actually meet. Since Java 14 it can tell you which expression was null:
String greeting = null;
try {
greeting.trim();
} catch (NullPointerException e) {
System.out.println(e.getMessage());
}
Map<String, List<String>> index = new HashMap<>();
try {
System.out.print(index.get("missing").get(0));
} catch (NullPointerException e) {
System.out.println(e.getMessage());
}Cannot invoke "String.trim()" because "greeting" is null
Cannot invoke "java.util.List.get(int)" because the return value of "java.util.Map.get(Object)" is nullThe second message is the one that saves hours: in a chained call it names which link returned null. Before Java 14 you got java.lang.NullPointerException and a line number, and on a line with three calls you reached for a debugger.
One catch worth knowing, because it's why this sometimes doesn't work for you: local variable names only appear if the class was compiled with debug information. Without -g, that first message reads because "<local1>" is null. Maven and Gradle both include debug info by default, so you normally get names — but a stripped production build loses them.
Interviewer's Next Move
The question after the question. This is what the round is actually testing.
1. "Which is better, checked or unchecked?"
Unchecked, for almost all new code, and the industry has already decided this. Checked exceptions don't compose — three libraries give you three unrelated types — so callers either propagate everything or swallow it. Spring wraps SQLException in unchecked DataAccessException; Hibernate went unchecked in 3.0; C# and Kotlin have no checked exceptions at all. Checked exceptions are defensible for a genuinely recoverable, local condition the caller must decide about.
2. "What did Java 8 change about this?"
It ended the debate mechanically. No standard functional interface declares throws, so a checked exception cannot escape a lambda. .map(p -> Files.readString(p)) does not compile. Every workaround converts the checked exception to unchecked at the lambda boundary, so any API meant to be used from a stream throws unchecked.
3. "Why prefer try-with-resources over finally, beyond brevity?"
Suppressed exceptions. In a hand-written finally, a close() that throws replaces the exception from the body, destroying the real cause. try-with-resources keeps the body's exception as primary and attaches the close failure via getSuppressed(). That's a correctness difference, not a style one.
4. "Can finally swallow an exception?"
Yes — a return, break, or continue inside finally discards an in-flight exception, as does throwing from finally. try { throw ... } finally { return 42; } returns 42 and the exception is gone. Never put control flow in finally.
5. "Should you catch Error, or Throwable?"
No. Error means the JVM is in a state you cannot fix — OutOfMemoryError, StackOverflowError. Catching Throwable catches those plus InterruptedException-driven control flow and every programming bug, which converts crashes into silent corruption. The one legitimate use is a top-level handler in a thread or request loop that logs and then rethrows or exits.
6. "How do you write a custom exception?"
Extend RuntimeException unless you have a specific reason not to, always provide a constructor taking a cause, and carry structured fields rather than formatting everything into the message. Overriding fillInStackTrace to skip stack capture is a real optimisation for control-flow exceptions on hot paths — and a smell that you're using exceptions for control flow.
7. "What happens if you catch InterruptedException and do nothing?"
You've discarded a cancellation signal and cleared the thread's interrupt flag, so nothing upstream can tell the thread was asked to stop — thread pools stop shutting down cleanly. Either propagate it, or call Thread.currentThread().interrupt() to restore the flag before returning.
Code traps
Trap A — predict before you run:
static int f() {
int x = 1;
try {
return x;
} finally {
x = 2;
}
}
Answer
1. The return value is evaluated and copied before finally runs, so mutating the local afterwards has no effect on what was already returned. Change it to return x; inside the finally and you get 2 — and you've also silently made the method swallow exceptions.
Trap B:
try {
throw new IllegalStateException("first");
} catch (IllegalStateException e) {
throw new IllegalArgumentException("second");
} finally {
System.out.println("finally ran");
}
Answer
Prints finally ran, then propagates IllegalArgumentException: second. finally runs even when the catch block itself throws, and the new exception replaces the old one with no chaining — "first" is lost unless you pass it as the cause: new IllegalArgumentException("second", e). Forgetting that cause argument is how stack traces end up starting nowhere near the real problem.
Common wrong answers
| Said in interviews | Reality |
|---|---|
| "Checked exceptions are best practice." | Every major framework since 2004 moved away from them. |
| "try-with-resources is just shorter." | It preserves the original exception; finally can destroy it. |
| "You can throw checked exceptions from a lambda." | No standard functional interface declares throws. |
| "NullPointerException is checked." | Unchecked — it's a RuntimeException. |
| "catch (Throwable) is the safe option." | It catches OutOfMemoryError and every bug, converting crashes into corruption. |
| "finally always runs, so exceptions are safe there." | It runs, and a return in it discards the exception. |
Check Yourself
Q1. What single fact determines whether an exception is checked?
Answer
Whether it sits under RuntimeException (or Error). Everything else below Exception is checked. No keyword, no annotation — position in the hierarchy alone.
Q2. Body throws IOException, close() throws IllegalStateException. What does the caller see, with and without try-with-resources?
Answer
With: IOException as primary, IllegalStateException reachable via getSuppressed(). Without: only the IllegalStateException — the IOException is gone entirely, which is why the pre-7 pattern produced undiagnosable failures.
Q3. Why does the arrival of lambdas count as an argument about checked exceptions?
Answer
Because functional interfaces declare no throws, so a checked exception physically cannot leave a lambda body. Every workaround wraps it into an unchecked exception. Once streams became the normal way to write Java, any API wanting to be stream-friendly had to throw unchecked — the language settled the design debate for everyone.
Practice
| Tier | Exercise | Time |
|---|---|---|
| Warm-up | Watch finally eat an exception | 5 min |
| Challenge | Get a checked exception through a stream | 20 min |
| Production | The incident with no cause in the logs | 45 min |
| Interview | Full round replay | 10 min |
What changed, and when
Version boundaries are where follow-ups live. Know the change and the behaviour it replaced.
- Java 7
try-with-resources, plus suppressed exceptions: if close() throws while an exception is already in flight, the original survives and the close failure is attached to it.
Before Java 7 (now gone): You closed in a finally block. If close() threw, its exception replaced the real one entirely — the actual cause was destroyed, and this was the single most common source of undiagnosable production failures in Java.
- Java 7
Multi-catch (catch (A | B e)) and precise rethrow, so a method can rethrow the specific checked types it caught rather than declaring throws Exception.
Before Java 7: Duplicated catch bodies, or one catch (Exception e) that swallowed types you never meant to handle.
- Java 8LTS
Lambdas made checked exceptions structurally unusable: no built-in functional interface declares throws, so a checked exception cannot escape a lambda body.
Before Java 8: Anonymous inner classes had the same constraint, but nobody wrote enough of them to care. Streams put this in front of every Java developer at once, and it is why new APIs stopped using checked exceptions.
- Java 9
try-with-resources accepts an existing effectively-final variable, so you can write try (conn) { }.
Before Java 9: You had to redeclare the resource inside the parentheses, even when you already held it.
- Java 14
Helpful NullPointerException messages name the exact expression that was null (JEP 358) — including which link of a chained call. Opt-in via -XX:+ShowCodeDetailsInExceptionMessages in 14, on by default from 15.
Before Java 14 (now gone): 'java.lang.NullPointerException' and a line number. On a line like a.b().c().d() you could not tell which of three calls returned null without a debugger or extra logging.
Practice ladder
Reading this page is not knowing it. Four tiers, ending in a real incident.
- Warm-up5 min
Watch finally eat an exception
One concept, guided. Near-impossible to fail.
- Challenge20 min
Get a checked exception through a stream
Edge cases. You have to reason, and two valid fixes differ.
- Production incident45 min
The incident with no cause in the logs
A real incident: symptom first, cause hidden, tradeoff at the end.
- Interview replay10 min
Full round replay — checked vs unchecked
Timed verbal replay with pass/fail criteria per follow-up.
Where this question goes next
- try with resources — not written yet
- custom exception design — not written yet
- optional vs null — not written yet
- stream exception handling — not written yet
Every runnable example above was compiled and executed against openjdk 21.0.11 on this build, and its output diffed against what this page claims. Last updated 2026-08-24.