W3docs

Java Checked vs. Unchecked Exceptions

The difference between checked and unchecked (runtime) exceptions in Java and when to use each.

Java Checked vs. Unchecked Exceptions

Java splits its exceptions into two groups, and the compiler treats them differently. Checked exceptions must be caught or declared; the compiler refuses to build code that ignores them. Unchecked exceptions don't have that requirement — the compiler lets them propagate silently and only the runtime catches them. The split is one of Java's most distinctive and most argued-about decisions, and understanding it shapes how you write robust code.

Which is which

Everything Throwable falls into one of three buckets:

  • Error — JVM-level catastrophe. Unchecked. Don't catch.
  • RuntimeException (a subclass of Exception) and all its subtypes. Unchecked.
  • Other subclasses of Exception. Checked.

The rule is "RuntimeException and Error plus their subtypes are unchecked; everything else under Throwable is checked." That's a class-tree distinction, not a configuration flag — there is no way to make a RuntimeException checked or an IOException unchecked.

What "checked" means in code

If a method body throws a checked exception, the method must do one of two things, or the program won't compile:

// Option 1: catch it
public void load() {
  try {
    String text = Files.readString(path);   // throws IOException
  } catch (IOException e) {
    // ...
  }
}

// Option 2: declare it
public void load() throws IOException {
  String text = Files.readString(path);
}

The compiler walks every method and checks: for each checked exception that could escape it, is there either a containing catch or a throws clause? If not, error.

Unchecked exceptions have no such requirement. You can throw an IllegalArgumentException from any method without touching the signature, and any caller is free to catch it or not.

The design idea

The motivation behind checked exceptions was: some failures are expected outcomes of the operation — a file might not exist, a network might be down — and the language should make sure callers think about them. Unchecked exceptions are for programming errors — null dereferences, index out of bounds, illegal arguments — where the right fix is to repair the code, not to add a try/catch.

That distinction sounds clean. In practice it bends:

  • Layered code multiplies declarations. A low-level IOException from a config loader doesn't stay there — every method that calls it has to handle or declare. By the time the exception reaches the controller, the signature lists six unrelated checked exceptions.
  • Lambdas don't allow them well. Stream.map(this::parseLine) won't compile if parseLine throws a checked exception, because Function's apply doesn't declare one.
  • Wrapping is everywhere. A common workaround is to catch a checked exception immediately and rethrow it as a runtime exception, which defeats the original purpose.

Most modern Java code uses fewer checked exceptions than the standard library does — sometimes none at all. New frameworks like Spring lean almost entirely on runtime exceptions for that reason.

When to use each

A defensible rule of thumb:

  • Use checked when the caller has a realistic recovery strategy that's specific to the failure — e.g. retrying on a network error, falling back on a parse failure. Force them to think about it.
  • Use unchecked when the failure is a bug (IllegalArgumentException, NullPointerException, IllegalStateException) or when no caller could reasonably recover.

When in doubt, prefer unchecked. The cost of getting it wrong is smaller, and you can always tighten later.

The catch-side experience

Whether an exception is checked changes what the catch looks like:

// checked: caller must catch or declare
try {
  parser.parse(path);
} catch (IOException e) {
  // handle
}

// unchecked: caller may catch but doesn't have to
try {
  return numbers.get(idx);
} catch (IndexOutOfBoundsException e) {
  return -1;
}

From the catch's perspective there's no behavioral difference — both blocks catch what they declare. The split only matters at the call site that didn't catch: a checked one would force a throws declaration; an unchecked one quietly propagates.

A common mistake: catching Exception

Because Exception is the parent of both checked and unchecked types (except Error), catch (Exception e) matches almost everything. This is often a code smell:

try { complexOperation(); }
catch (Exception e) { log("failed"); }    // hides bugs and real failures alike

The problem isn't that it catches — it's that it catches too much. A NullPointerException here indicates a bug; an IOException indicates a real recoverable failure; a RuntimeException from a library you don't control could be anything. Treating them identically usually means handling none of them well. Prefer catching the specific types you have a plan for.

A worked example

A small program with two methods: one declares a checked IOException and the other throws an unchecked IllegalArgumentException. The compiler treats them differently — only the checked one forces handling at the call site.

java— editable, runs on the server

Notice three things. Remove the throws java.io.IOException from maybeReadFile and the file won't compile. Remove the surrounding try/catch from the first call and the file won't compile either. But there's no try/catch around the final requirePositive(-7) — and that compiles fine, even though the call will crash the program. That's the asymmetric treatment in one screen.

What's next

We've been talking about types like IOException, RuntimeException, Error — but how do they actually relate? The class tree is small and worth memorising. Continue to Java exception hierarchy.

Practice

Practice

Which statement about checked exceptions is correct?