ByteScrollGet the app
☰ Topics
checked-vs-unchecked17 / 200‹›
JAVA / EXCEPTIONS3 minute read

Checked vs unchecked exceptions — when to use which?

Medium

Checked exceptions (Exception but not RuntimeException) must be caught or declared, so the compiler forces callers to think about them. Use them for failures a caller can act on. Unchecked ones (RuntimeException, Error) signal bugs or failures nobody nearby can fix.

How it works

  1. The hierarchy decides. Everything throwable extends Throwable.
    • Error (OutOfMemoryError, StackOverflowError): unchecked, JVM-level trouble.
    • RuntimeException and its subclasses (NullPointerException, IllegalArgumentException): unchecked.
    • Every other Exception (IOException, SQLException, TimeoutException): checked.
  2. Compile-time rule. A method that can throw a checked exception must either catch it or list it in throws. Unchecked exceptions need neither. The JVM treats both kinds the same at runtime.
  3. When to choose checked. The caller can reasonably recover and you want to force the decision: retry the download, ask for another file, fall back to a cache.
  4. When to choose unchecked.
    • Programming errors: bad arguments, broken state, null where it isn't allowed. The fix is in the code, not a catch.
    • Failures that only a top-level handler can deal with (log, return 500).
  5. The modern lean. Lambdas and streams can't propagate checked exceptions through standard functional interfaces, and frameworks like Spring translate checked exceptions into unchecked ones. Many teams keep checked exceptions for a few clearly recoverable cases only.
ThrowableError(unchecked)ExceptionRuntimeException (unchecked)IOException,SQLException,... (checked)NullPointerExceptionIllegalArgumentExceptionOutOfMemoryError
ThrowableError(unchecked)ExceptionRuntimeException (unchecked)IOException,SQLException,... (checked)NullPointerExceptionIllegalArgumentExceptionOutOfMemoryError

Example

Example.javaJava
// Checked: the caller can decide to retry or pick another file
static String loadTemplate(Path path) throws IOException {
    return Files.readString(path);
}

// Unchecked: a caller passing a negative quantity has a bug
static void reserve(String sku, int qty) {
    if (qty <= 0) throw new IllegalArgumentException("qty must be positive, got " + qty);
    // ...
}

// Checked exceptions don't fit inside a lambda; wrap them, keeping the cause
List<String> bodies = paths.stream()
        .map(p -> {
            try { return Files.readString(p); }
            catch (IOException e) { throw new UncheckedIOException("cannot read " + p, e); }
        })
        .toList();

Edge cases

  • When you override a method, its throws clause can drop checked exceptions or list subclasses of them, but it can't add new or wider ones. Unchecked exceptions aren't limited.
  • With try-with-resources, a failure during cleanup never hides the original problem: the exception from the try block propagates, and any exception from closing the resource rides along in getSuppressed().
  • catch (Exception e) also catches every RuntimeException, including the bugs you wanted to surface.
  • Generics can smuggle a checked exception out undeclared (the "sneaky throw" trick). It compiles, but callers can't catch it by type without tricks.

Common mistakes

  • Empty catch blocks, or e.printStackTrace() and carry on.
  • Wrapping without the cause (new RuntimeException(e.getMessage())), which throws the original stack trace away.
  • Catching Error or Throwable in ordinary code. There's rarely a sane recovery from OutOfMemoryError.

Likely follow-up

"How would you design exceptions for a service layer?" A small set of unchecked domain exceptions (OrderNotFoundException, PaymentDeclinedException) that carry context, thrown from the service and mapped to responses in one place, such as a Spring @ControllerAdvice.

Get every deep dive in the app

Coming soon to the App StoreComing soon to Google Play