Topic 7.3
Checked vs Unchecked Exceptions
In one line
Checked exceptions (subclasses of Exception but not RuntimeException) must be caught or declared with throws, and the compiler enforces it. Unchecked exceptions (RuntimeException, Error and their subclasses) need no declaration because they usually mean a bug or a broken environment.
Think of it like this
A parcel delivery. Some problems are expected and not your fault: the customer isn't home, the address is a locked building. The delivery company makes every driver carry a plan for those, and the form won't let them skip it. Other problems are mistakes: the driver wrote the wrong house number. No form can plan for every mistake; the fix is to drive more carefully. Checked exceptions are the first kind, and the compiler makes you plan for them. Unchecked exceptions are the second kind.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Checked exception
- An exception the compiler checks for: you must catch it or declare it with
throws. Example:IOException. - Unchecked exception
- An exception the compiler doesn't make you handle:
RuntimeException,Errorand their subclasses. Example:NullPointerException. - Catch or declare
- Java's rule for checked exceptions: handle it here with
try/catch, or announce it withthrowsso the caller must. - `throws` clause
- The part of a method signature, like
throws IOException, that lists checked exceptions the method may pass to its caller. - Recoverable
- A problem the program can sensibly respond to, for example by retrying, choosing another file, or showing a message.
- Programming error
- A bug in the code itself, such as using
nullor a bad index. The fix is to change the code. - Wrapping
- Catching one exception and throwing a different one that keeps the first as its cause, for example turning
IOExceptionintoUncheckedIOException. - Functional interface
- An interface with one abstract method, such as
Consumer<T>, that a lambda can implement (Phase 10).
Step by step
01Meet the compiler's rule
The FileReader constructor is declared throws FileNotFoundException, a checked exception. Calling it without catching or declaring is a compile error, before the program ever runs.
Notice that the compiler doesn't know whether notes.txt exists. It just sees that this call can fail in a way you're expected to plan for.
import java.io.*;
public class Main {
public static void main(String[] args) {
FileReader r = new FileReader("notes.txt");
}
}02Option 1: catch it
Handle it where you are, if you know what to do. Here, a missing file means 'use the defaults'. After the catch, callers of this method never hear about FileNotFoundException.
static String loadSettings() {
try (FileReader r = new FileReader("settings.txt")) {
return "custom";
} catch (IOException e) { // covers FileNotFoundException too
return "defaults";
}
}03Option 2: declare it
If this method can't sensibly handle the failure, add throws and pass the decision up. Now every caller faces the same choice: catch it or declare it too.
Declaring is not lazy: it's correct when the caller knows more (should a missing file abort the import, skip one row, or prompt the user?). main can also declare throws Exception, which is fine for small programs: an uncaught exception just ends the program with a stack trace.
static String readFirstLine(String path) throws IOException {
try (BufferedReader r = new BufferedReader(new FileReader(path))) {
return r.readLine();
}
}04Draw the line in the hierarchy
The checked/unchecked status is inherited. Everything under RuntimeException is unchecked, everything under Error is unchecked, everything else is checked. You decide the status of your own exception classes by choosing which one to extend (Topic 7.5).
05What the compiler actually checks
For every statement, javac computes which checked exceptions it can throw: from throw statements (using the static type of the expression), from the throws clauses of methods and constructors it calls, and from resource close() methods in try-with-resources. Then it checks each one is caught by an enclosing catch or listed in the method's throws.
The same analysis gives you two extra errors: a catch for a checked exception the try can't throw (is never thrown in body of corresponding try statement), and an overriding method that declares a broader checked exception than the method it overrides.
06Overriding can't add checked exceptions
If Shape.area() declares nothing, a subclass's area() can't declare throws Exception. Code written against Shape was promised there's nothing to handle; a subclass mustn't break that promise (the Liskov substitution principle from Phase 5).
An override may declare fewer or narrower checked exceptions, and any unchecked ones it likes.
07Lambdas: the friction point
forEach takes a Consumer<T>, whose accept method declares no exceptions. A lambda body calling a throws IOException method can't pass it on, so it doesn't compile.
Fix it by catching inside the lambda and rethrowing as UncheckedIOException, or by using a plain for loop, where the enclosing method can simply declare throws IOException. The loop is often the clearer choice.
Try it yourself
- 1
Classify a few more
Add
new java.sql.SQLException(),new ArrayIndexOutOfBoundsException(),new StackOverflowError()andnew java.util.NoSuchElementException()to the classification example. Predict each column before running. - 2
Remove the throws clause
In the first example, delete
throws IOExceptionfromloadConfig. Predict the compiler error message and the line it points to, then compile. Put it back, and delete the try/catch inmaininstead. What does the error say now? - 3
Rewrite the lambda as a loop
Replace the
forEachin the lambda example with an enhancedforloop and makemaindeclarethrows IOException. Which output lines still appear, and what happens whenbfails?
Code & diagrams
Expected output
hello
loaded app.properties
checked, caught: not a config file: app.txt
unchecked, caught: Index 5 out of bounds for length 2Expected output
Exception checked
IOException checked
FileNotFoundException checked
InterruptedException checked
TimeoutException checked
CloneNotSupportedException checked
NullPointerException unchecked
IllegalArgumentException unchecked
NumberFormatException unchecked
ClassCastException unchecked
UncheckedIOException unchecked
AssertionError uncheckedUncheckedIOException(cause) uses cause.toString() as its message. getCause() is typed IOException, so no cast is needed.
Expected output
fetched A
wrapped: java.io.IOException: server refused b
cause: IOException - server refused bLombok's @SneakyThrows generates this. It works because the JVM never checks throws clauses.
// Shown to explain the JVM, not as a recommendation.
@SuppressWarnings("unchecked")
static <E extends Throwable> void sneakyThrow(Throwable t) throws E {
throw (E) t; // unchecked cast: erased, so no check happens at run time
}
static void readQuietly() { // declares nothing...
Main.<RuntimeException>sneakyThrow(new java.io.IOException("disk gone"));
} // ...yet throws a checked IOException
// Callers can't even write catch (IOException e) around readQuietly():
// javac says the exception "is never thrown in body of corresponding try statement".Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Ignore a checked exception
Call new FileReader("notes.txt") in main without a try/catch or a throws clause.
Break #2
Widen the exceptions in an override
With class A { void m() { } }, write class B extends A { @Override void m() throws Exception { } }.
Myth vs fact
Myth
The JVM enforces checked exceptions.
Fact
Only the Java compiler does. The JVM ignores throws clauses; Kotlin code, reflection (Method.invoke wraps, but Class.newInstance didn't) and generic tricks can throw checked exceptions from methods that don't declare them.
Myth
Unchecked exceptions shouldn't be caught.
Fact
You don't have to, but you often should at a boundary: a web framework catches every RuntimeException from a request handler to return a 500 response instead of killing the server thread. What you shouldn't do is catch them to paper over bugs.
Myth
RuntimeException means 'happens at run time' and checked ones happen at compile time.
Fact
All exceptions happen at run time. 'Checked' only means the compiler checks that you planned for them. The name RuntimeException is historical.
Myth
Checked exceptions were a mistake and should never be used.
Fact
They're debated, not dead. They are still the right tool when callers can genuinely recover (a missing file the user can re-pick, InterruptedException for cancellation). The common advice is to use them sparingly, not never.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
The
throwsclause is stored in the class file'sExceptionsattribute, used by javac when compiling callers and by reflection (Method.getExceptionTypes()). The verifier never checks it. That's whyThread.stop-style asynchronous exceptions, native code, and other JVM languages can deliver checked exceptions anywhere. - ▸
Generic methods can declare a type-variable exception:
<E extends Exception> void forEachChecked(ThrowingConsumer<T, E> c) throws E. javac infersEfrom the lambda, and if the lambda throws nothing checked,Eis inferred asRuntimeException(the 'throws' inference variables of JLS chapter 18). This is how libraries offer lambda-friendly APIs that keep checked exceptions checked. - ▸
InterruptedExceptionis checked on purpose: it is Java's cancellation signal. Catching it and doing nothing loses the interrupt. Either rethrow it, or restore the flag withThread.currentThread().interrupt()before returning (the concurrency phase covers this). - ▸
Reflection wraps:
Method.invokethrowsInvocationTargetException(checked) whosegetCause()is the real exception, and dynamic proxies wrap undeclared checked exceptions inUndeclaredThrowableException. Always unwrap before logging or deciding what to do.
Remember this
- 1
The rule, called catch or declare (JLS 11.2): if code can throw a checked exception, the enclosing method must either catch it with a
try/catch, or declare it in its signature withthrows, passing the duty on to its callers. If you do neither, the program doesn't compile:unreported exception IOException; must be caught or declared to be thrown. - 2
Which exceptions are checked? Every
Throwablethat is not aRuntimeException(or subclass) and not anError(or subclass). SoExceptionitself,IOException,FileNotFoundException,SQLException,InterruptedException,CloneNotSupportedExceptionandTimeoutExceptionare checked.NullPointerException,IllegalArgumentException,IllegalStateException,ArithmeticException,ClassCastException,IndexOutOfBoundsException,UnsupportedOperationExceptionandConcurrentModificationExceptionare unchecked. - 3
The design idea: checked exceptions describe recoverable conditions outside the program's control: a file is missing, the network timed out, the database said no. The caller can reasonably do something (retry, ask for another file, show a message). Unchecked exceptions describe programming errors (passing
null, a bad index, a wrong cast) that should be fixed in code rather than handled at run time, plusErrors, which mean the JVM itself is failing. - 4
Checked exceptions are a compile-time feature only. The JVM doesn't know or care whether an exception is checked: the
throwsclause is stored as metadata (theExceptionsattribute) and never verified at run time. Code compiled from other JVM languages (Kotlin, Groovy, Scala) can throw anIOExceptionfrom a method that doesn't declare it, and so can a few Java tricks (the generic 'sneaky throw'). - 5
Checked exceptions and lambdas don't mix well. The functional interfaces in
java.util.function(Function,Consumer,Supplier...) declare no checked exceptions, so a lambda body that calls a method declaredthrows IOExceptiondoesn't compile. The usual fix is to catch inside the lambda and wrap in an unchecked exception, such asUncheckedIOException(Java 8). You'll meet this again with streams in Phase 10. - 6
This split is unique to Java and is still debated. C#, Kotlin and Scala dropped checked exceptions entirely. Modern Java style uses them sparingly: libraries and frameworks (Spring, JPA) mostly throw unchecked exceptions, and newer JDK APIs often wrap I/O failures in
UncheckedIOException. Use checked exceptions when the caller can and should recover; otherwise use unchecked ones.
Explain it without notes
What is the difference between checked and unchecked exceptions, and how does Java decide which is which?
What does 'catch or declare' mean, and who enforces it?
When would you design an API to throw a checked exception, and when an unchecked one?
Why do checked exceptions cause trouble with lambdas, and how do you deal with it?
Why can't an overriding method throw a broader checked exception?
Practice
Write static boolean isChecked(Class<? extends Throwable> c) using RuntimeException.class.isAssignableFrom(c) and Error.class.isAssignableFrom(c). Test it on IOException.class, IllegalStateException.class and OutOfMemoryError.class.
Write a method int parsePositive(String s) that throws an unchecked IllegalArgumentException for a non-positive number and lets NumberFormatException pass through. Call it in main with "7", "-3" and "x", catching each problem and printing its simple class name and message.
Given static int lengthOf(String path) throws IOException { if (path.isEmpty()) throw new IOException("empty path"); return path.length(); }, sum the lengths of List.of("a.txt", "", "bb.txt") with forEach and a wrapping lambda, and print the wrapped message when it fails.
Trade-offs
- ↔
Checked exceptions make failure part of the method's contract and force callers to think about it, but they spread
throwsclauses through every layer, and many callers end up writing empty catches just to compile. - ↔
Unchecked exceptions keep signatures clean and work with lambdas and frameworks, but nothing reminds callers that a failure is possible: they must read the docs, and recoverable failures can go unhandled.
- ↔
Wrapping checked in unchecked (
UncheckedIOException) keeps lambdas and interfaces simple, but you lose the compiler's help and must remember to unwrap the cause at the boundary where you handle it.
Done when you can
Done when you can classify any exception class as checked or unchecked from the hierarchy.
Done when you can fix an 'unreported exception' error both ways: catching and declaring.
Done when you can explain why the JVM doesn't enforce checked exceptions.
Done when you can call a checked-exception method from a lambda by wrapping and unwrapping.
Done when you can state the overriding rule for
throwsclauses and why it exists.Done when you can argue when an API should throw checked vs unchecked exceptions.