Ch. 7 · Java

Checked vs Unchecked Exceptions in Java

Checked exceptions must be caught or declared; unchecked ones needn't be. See the compiler rules, the lambda problem and when to use each.

~7 min readbeginnerupdated Oct 20, 2026

A checked exception is one the compiler makes you deal with: if a method can throw it, the caller must either catch it or declare it with throws, or the code won’t compile. An unchecked exception carries no such obligation. In Java the split is by class: RuntimeException, Error and their subclasses are unchecked, and every other subclass of Throwable (in practice, Exception and its non-runtime subclasses) is checked.

IOException, SQLException and InterruptedException are checked. NullPointerException, IllegalArgumentException, IllegalStateException and ArithmeticException are unchecked. That’s the definition, and it gets you through the first question. The follow-ups test whether you know what the compiler actually enforces, where checked exceptions get awkward (lambdas and overriding), and how you’d decide which kind to throw.

The rules at a glance

Checked Unchecked
Classes Exception and subclasses, except RuntimeException RuntimeException, Error and subclasses
Must be caught or declared Yes, compile error otherwise No
Checked by javac only Nobody
Can appear in throws Yes, required if not caught Allowed, purely documentation
Overriding a method Can’t add new or broader ones Free to throw any
Inside Function, Supplier, etc. Not allowed Allowed
Usually means An expected failure outside the program’s control A bug, broken precondition or JVM problem
Examples IOException, SQLException, TimeoutException NullPointerException, IllegalArgumentException, UncheckedIOException

The hierarchy is worth drawing on a whiteboard:

Throwable
├── Error                      unchecked (OutOfMemoryError, StackOverflowError)
└── Exception                  checked
    ├── IOException            checked
    ├── SQLException           checked
    └── RuntimeException       unchecked
        ├── NullPointerException
        ├── IllegalArgumentException
        └── IllegalStateException
Text

Error sits outside Exception and is unchecked too. It represents problems the application usually can’t recover from, which is why nothing forces you to catch it.

What the compiler enforces

The examples below are illustrative Java 17; they weren’t compiled for this page, and the error messages are the ones javac prints, trimmed to the important line.

Call a method that declares a checked exception without handling it, and compilation stops:

String loadTemplate(Path path) {
    return Files.readString(path);
}
// error: unreported exception IOException; must be caught or declared to be thrown
java

You have two ways out. Declare it, passing the obligation to your caller:

String loadTemplate(Path path) throws IOException {
    return Files.readString(path);
}
java

Or catch it and do something meaningful, such as falling back or translating it:

String loadTemplate(Path path) {
    try {
        return Files.readString(path);
    } catch (NoSuchFileException e) {
        return DEFAULT_TEMPLATE;                         // a real recovery
    } catch (IOException e) {
        throw new UncheckedIOException("Cannot read template " + path, e);
    }
}
java

The compiler also works in the other direction. Catching a checked exception that the try block can’t throw is an error (exception IOException is never thrown in body of corresponding try statement), which stops dead catch blocks from piling up. Catching Exception is always allowed, since it includes unchecked exceptions that could come from anywhere.

Unchecked exceptions get none of this. Integer.parseInt(text) can throw NumberFormatException, and the compiler says nothing whether you handle it or not.

It’s a compile-time feature only

The JVM doesn’t know about checked exceptions. At runtime, every exception unwinds the stack the same way, and bytecode can throw a checked exception from a method that never declared it. Other JVM languages rely on this: Kotlin and Scala have no checked exceptions, so a Kotlin function can throw IOException into your Java code without any throws clause. Inside Java, generic tricks (often called “sneaky throws”) and some libraries do the same.

This matters in interviews because it corrects a common misconception: checked exceptions aren’t “safer at runtime” or handled differently by the JVM. They’re a static check that a caller has thought about a failure, nothing more.

Where checked exceptions get in the way

Overriding and interfaces

An overriding method may throw fewer or narrower checked exceptions than the method it overrides, never new or broader ones. Callers holding a reference to the parent type only prepared for what the parent declared:

interface TemplateSource {
    String load(String name);                // declares nothing
}

class FileTemplateSource implements TemplateSource {
    public String load(String name) throws IOException {   // compile error
        return Files.readString(Path.of(name));
    }
}
// error: load(String) in FileTemplateSource cannot implement load(String) in TemplateSource
//   overridden method does not throw IOException
java

The interface’s designer decided the contract. The implementation must catch and translate, or the interface must change to declare the exception. That’s why AutoCloseable.close() declares throws Exception (implementations can narrow it) and Closeable.close() declares throws IOException.

Lambdas and streams

The standard functional interfaces (Function, Supplier, Consumer, Predicate) declare no checked exceptions, so a lambda or method reference passed to them can’t throw one:

List<String> bodies = paths.stream()
        .map(Files::readString)              // compile error: unreported exception IOException
        .toList();
java

The usual fix is to wrap inside the lambda and convert to an unchecked exception, keeping the cause:

List<String> bodies = paths.stream()
        .map(p -> {
            try {
                return Files.readString(p);
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        })
        .toList();
java

UncheckedIOException exists in the JDK (since Java 8) precisely for this. If you do this often, a small helper or your own functional interface that declares throws IOException keeps the call sites readable. Expect an interviewer to ask about this one; it’s the most common day-to-day friction with checked exceptions.

How to choose which kind to throw

Ask whether a typical caller can do something useful about the failure. If yes, and it’s an expected part of the operation (a missing file a user picked, a payment declined, a remote service timing out), a checked exception documents that in the signature and forces callers to plan for it. If the failure means the caller did something wrong (passed null, called methods in the wrong order, violated a documented range), throw an unchecked exception: there’s no sensible recovery except fixing the code, and forcing every caller to write a catch for it just adds noise.

In practice, much of the Java ecosystem has drifted toward unchecked exceptions for application code. Spring translates SQLException into its unchecked DataAccessException hierarchy, JPA throws unchecked PersistenceException, and many teams define unchecked domain exceptions so that intermediate layers don’t have to declare failures they can’t act on. Checked exceptions remain common at I/O boundaries. Either position is defensible in an interview if you explain the trade-off: checked exceptions make failure visible in signatures, at the cost of verbosity and awkwardness with lambdas.

Three things to avoid whichever kind you use:

  • Swallowing. catch (IOException e) { } turns a failure into wrong behaviour later. If you catch, recover or rethrow.
  • Dropping the cause. throw new ServiceException(e.getMessage()) loses the stack trace. Pass e as the cause.
  • Catching InterruptedException and moving on. Restore the flag with Thread.currentThread().interrupt() so code further up knows the thread was asked to stop.

Where those catch blocks should live, and how to translate between layers, is covered in Java exceptions and recovery boundaries.

Follow-up questions you may hear

Does catch (Exception e) catch unchecked exceptions? Yes, RuntimeException is a subclass of Exception. It doesn’t catch Errors; only catch (Throwable t) does.

Can you declare an unchecked exception in throws? Yes. It’s legal and sometimes used as documentation, but the compiler ignores it for callers.

What does catch (Exception e) { throw e; } declare? Since Java 7, the compiler does precise rethrow analysis: if the try block can only throw IOException, the method needs to declare only IOException, not Exception, as long as e isn’t reassigned.

Is Exception itself checked? Yes. Declaring throws Exception forces every caller to handle or redeclare it, which is why broad throws Exception clauses are discouraged outside of tests and main.

Interview exercise: classify and fix

A colleague writes a ConfigLoader that implements Supplier<Config>. Its get() reads a YAML file with a method declared as Config parse(Path p) throws IOException and throws IllegalArgumentException when the file’s port is outside 1 to 65535. The code doesn’t compile. Which exception is the problem, how do you fix it, and are both exception types well chosen?

Answer

The IOException is the problem. It’s checked, and Supplier.get() declares no checked exceptions, so the implementation can’t let it escape (and can’t add a throws clause, because overriding methods can’t introduce new checked exceptions). IllegalArgumentException is unchecked, so it compiles fine.

Fix it by catching inside get() and wrapping with the cause, throw new UncheckedIOException("Cannot read " + path, e), or by not implementing Supplier and exposing a plain Config load() throws IOException method so callers see the failure in the signature. The second is better if callers are expected to handle a missing config (for example, by falling back to defaults); the first suits startup code where the only reaction is to fail fast.

On the choices: wrapping I/O failure is reasonable. IllegalArgumentException is a weak fit, because the bad port isn’t an argument the caller passed, it’s invalid file content. A dedicated unchecked InvalidConfigException naming the file and the field would be clearer, and it would let a startup handler print a precise message instead of a generic stack trace.

Further reading

More in Java

read ✓Java · mid

Java Exceptions and Recovery Boundaries

Checked vs unchecked exceptions in Java, where to catch, when to wrap with a cause, and how to avoid swallowing failures at the wrong layer.

~6 min readread →
read ✓Java · hard

Java CompletableFuture Error Composition

How exceptions travel through CompletableFuture chains, when to use exceptionally, handle or whenComplete, and why causes arrive wrapped.

~7 min readread →
read ✓Java · mid

Java Try-With-Resources and Cleanup

How try-with-resources closes resources in reverse order, keeps the primary exception with suppressed close failures, and beats finally.

~6 min readread →
esc