Command Palette

Search for a command to run...

PHASE 7Intermediate ~30 min· topic 3 of 8

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, Error and their subclasses. Example: NullPointerException.
Catch or declare
Java's rule for checked exceptions: handle it here with try/catch, or announce it with throws so 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 null or 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 IOException into UncheckedIOException.
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.

Main.javawhole filejava
import java.io.*;

public class Main {
    public static void main(String[] args) {
        FileReader r = new FileReader("notes.txt");
    }
}
terminal
$ javac Main.java
── expected output ──
Main.java:5: error: unreported exception FileNotFoundException; must be caught or declared to be thrown
FileReader r = new FileReader("notes.txt");
^
1 error

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.

Main.javawhole filejava
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.

Main.javawhole filejava
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).

Draw the line in the hierarchydiagram
Rendering diagram…

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.

terminal
$ javac Main.java
── expected output ──
Main.java:2: error: m() in B cannot override m() in A
class B extends A { @Override void m() throws Exception { } }
^
overridden method does not throw Exception
1 error

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.

terminal
$ javac Main.java
── expected output ──
Main.java:10: error: unreported exception IOException; must be caught or declared to be thrown
List.of("a", "b").forEach(id -> System.out.println(fetch(id)));
^
1 error

Try it yourself

  1. 1

    Classify a few more

    Add new java.sql.SQLException(), new ArrayIndexOutOfBoundsException(), new StackOverflowError() and new java.util.NoSuchElementException() to the classification example. Predict each column before running.

  2. 2

    Remove the throws clause

    In the first example, delete throws IOException from loadConfig. Predict the compiler error message and the line it points to, then compile. Put it back, and delete the try/catch in main instead. What does the error say now?

  3. 3

    Rewrite the lambda as a loop

    Replace the forEach in the lambda example with an enhanced for loop and make main declare throws IOException. Which output lines still appear, and what happens when b fails?

Code & diagrams

Checked: catch or declare. Unchecked: your choice New tab
Sign in to run this example in your browser.

Expected output

hello
loaded app.properties
checked, caught: not a config file: app.txt
unchecked, caught: Index 5 out of bounds for length 2
Which exceptions are checked? New tab
Sign in to run this example in your browser.

Expected output

Exception                   checked
IOException                 checked
FileNotFoundException       checked
InterruptedException        checked
TimeoutException            checked
CloneNotSupportedException  checked
NullPointerException        unchecked
IllegalArgumentException    unchecked
NumberFormatException       unchecked
ClassCastException          unchecked
UncheckedIOException        unchecked
AssertionError              unchecked
Checked exceptions inside a lambda: wrap them New tab

UncheckedIOException(cause) uses cause.toString() as its message. getCause() is typed IOException, so no cast is needed.

Sign in to run this example in your browser.

Expected output

fetched A
wrapped: java.io.IOException: server refused b
cause: IOException - server refused b
The 'sneaky throw': checked is compile-time only (fragment)java

Lombok'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.

terminal
$ javac Main.java
── what you'll see ──
Main.java:4: error: unreported exception FileNotFoundException; must be caught or declared to be thrown
FileReader r = new FileReader("notes.txt");
^
1 error

Break #2

Widen the exceptions in an override

With class A { void m() { } }, write class B extends A { @Override void m() throws Exception { } }.

terminal
$ javac Main.java
── what you'll see ──
Main.java:2: error: m() in B cannot override m() in A
class B extends A { @Override void m() throws Exception { } }
^
overridden method does not throw Exception
1 error

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 throws clause is stored in the class file's Exceptions attribute, used by javac when compiling callers and by reflection (Method.getExceptionTypes()). The verifier never checks it. That's why Thread.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 infers E from the lambda, and if the lambda throws nothing checked, E is inferred as RuntimeException (the 'throws' inference variables of JLS chapter 18). This is how libraries offer lambda-friendly APIs that keep checked exceptions checked.

  • ▸

    InterruptedException is checked on purpose: it is Java's cancellation signal. Catching it and doing nothing loses the interrupt. Either rethrow it, or restore the flag with Thread.currentThread().interrupt() before returning (the concurrency phase covers this).

  • ▸

    Reflection wraps: Method.invoke throws InvocationTargetException (checked) whose getCause() is the real exception, and dynamic proxies wrap undeclared checked exceptions in UndeclaredThrowableException. Always unwrap before logging or deciding what to do.

Remember this

  1. 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 with throws, 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. 2

    Which exceptions are checked? Every Throwable that is not a RuntimeException (or subclass) and not an Error (or subclass). So Exception itself, IOException, FileNotFoundException, SQLException, InterruptedException, CloneNotSupportedException and TimeoutException are checked. NullPointerException, IllegalArgumentException, IllegalStateException, ArithmeticException, ClassCastException, IndexOutOfBoundsException, UnsupportedOperationException and ConcurrentModificationException are unchecked.

  3. 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, plus Errors, which mean the JVM itself is failing.

  4. 4

    Checked exceptions are a compile-time feature only. The JVM doesn't know or care whether an exception is checked: the throws clause is stored as metadata (the Exceptions attribute) and never verified at run time. Code compiled from other JVM languages (Kotlin, Groovy, Scala) can throw an IOException from a method that doesn't declare it, and so can a few Java tricks (the generic 'sneaky throw').

  5. 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 declared throws IOException doesn't compile. The usual fix is to catch inside the lambda and wrap in an unchecked exception, such as UncheckedIOException (Java 8). You'll meet this again with streams in Phase 10.

  6. 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

01

What is the difference between checked and unchecked exceptions, and how does Java decide which is which?

02

What does 'catch or declare' mean, and who enforces it?

03

When would you design an API to throw a checked exception, and when an unchecked one?

04

Why do checked exceptions cause trouble with lambdas, and how do you deal with it?

05

Why can't an overriding method throw a broader checked exception?

Practice

01

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.

02

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.

03

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 throws clauses 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 throws clauses and why it exists.

  • Done when you can argue when an API should throw checked vs unchecked exceptions.