RizTech Academy logo
RizTech Academy
Exceptions and Error HandlingLesson 3 of 625 min

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

  1. Meet the compiler error. Call Files.readAllLines without handling anything. Read the message. Then satisfy it both ways — throws and try/catch — and say which you would choose for a method called from main.

  2. Hit the lambda wall. Put Files.readString inside a map. 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.

  3. Write the empty catch, then delete it. Catch an IOException, call printStackTrace(), 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.

  4. Wrap at the right boundary. Write loadDeliveries(Path) that wraps IOException in an unchecked exception naming the path. Compare its message with one wrapped immediately inside the lowest-level call.

  5. Break the override rule. Write an interface method with no throws, then try to implement it with a method that throws IOException. Read the error and connect it to why lambdas cannot throw checked exceptions.

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