Checked vs Unchecked Exceptions in Java

4 minute read

Java’s exception system is divided into two camps: checked and unchecked exceptions. Understanding the difference is essential for writing robust code — and it’s one of the most debated topics in Java.


The Throwable Hierarchy

                    Throwable
                   /         \
              Error          Exception
             /    \         /          \
    OutOfMemoryError   IOException   RuntimeException
    StackOverflowError  SQLException  NullPointerException
    ...                 ...           IllegalArgumentException
                                      ArrayIndexOutOfBoundsException
                                      ...
Category Checked? Compiler Enforces? You Should Catch?
Error ❌ No Rarely (unrecoverable)
Exception (non-Runtime) ✅ Yes Yes
RuntimeException ❌ No Sometimes

Checked Exceptions

Checked exceptions extend Exception (but not RuntimeException). The compiler forces you to handle or declare them.

// FileNotFoundException is checked — must handle or declare
public String readFile(String path) throws IOException {
    return Files.readString(Path.of(path));
}

Common Checked Exceptions

Exception Typical Cause
IOException File/network I/O failure
FileNotFoundException File doesn’t exist
SQLException Database error
ClassNotFoundException Class not found at runtime
InterruptedException Thread interrupted
ParseException Parsing error (dates, etc.)

Why They Exist

Checked exceptions represent recoverable conditions that a well-written program should anticipate:

  • File might not exist → prompt user for another path
  • Network might be down → retry or show error message
  • Database might be unavailable → use fallback

Unchecked Exceptions

Unchecked exceptions extend RuntimeException. The compiler does not require handling them.

// No throws declaration needed
public int divide(int a, int b) {
    return a / b;  // may throw ArithmeticException
}

Common Unchecked Exceptions

Exception Typical Cause
NullPointerException Calling method on null
IllegalArgumentException Bad method parameter
IllegalStateException Wrong object state
ArrayIndexOutOfBoundsException Bad array index
ClassCastException Invalid cast
NumberFormatException Bad string-to-number conversion
UnsupportedOperationException Method not supported
ConcurrentModificationException Collection modified during iteration

Why They Exist

Unchecked exceptions represent programming errors — bugs that should be fixed in code, not caught at runtime:

  • NullPointerException → add null check
  • ArrayIndexOutOfBoundsException → fix the index logic
  • IllegalArgumentException → validate before calling

Side-by-Side Comparison

Feature Checked Unchecked
Extends Exception RuntimeException
Compiler enforces? ✅ Must handle/declare ❌ Optional
Represents Recoverable conditions Programming errors
Examples IOException, SQLException NullPointerException, IllegalArgumentException
Typical handling try-catch or throws Fix the code / validate inputs

The Great Debate

Arguments FOR Checked Exceptions

  1. Self-documenting — the method signature tells you what can go wrong.
  2. Compiler safety net — you can’t forget to handle a known failure.
  3. Forced consideration — you must think about error recovery.

Arguments AGAINST Checked Exceptions

  1. Verbose — leads to boilerplate try-catch blocks everywhere.
  2. Leaky abstractions — throws SQLException in a service layer exposes implementation.
  3. Stream-hostile — lambdas and streams don’t play well with checked exceptions.
  4. Catch-and-ignore — developers often write empty catch blocks just to compile.

Modern consensus: Use checked exceptions sparingly — only when the caller can realistically recover. Default to unchecked for most cases.


Converting Between Checked and Unchecked

Wrapping Checked in Unchecked

public User findUser(long id) {
    try {
        return database.query(id);
    } catch (SQLException e) {
        // Wrap checked exception in unchecked
        throw new RuntimeException("Failed to find user " + id, e);
    }
}

Or create a domain-specific unchecked exception:

public class DatabaseException extends RuntimeException {
    public DatabaseException(String message, Throwable cause) {
        super(message, cause);
    }
}

// Usage
throw new DatabaseException("Query failed", sqlException);
@SuppressWarnings("unchecked")
static <T extends Throwable> void sneakyThrow(Throwable t) throws T {
    throw (T) t;
}

This bypasses the compiler — used by Lombok’s @SneakyThrows. Generally avoid this.


Handling Checked Exceptions in Streams

Streams and checked exceptions don’t mix:

// ❌ Won't compile — lambda can't throw checked exception
list.stream()
    .map(path -> Files.readString(path))  // IOException!
    .toList();

Solution 1: Wrapper Method

private String readFileSafe(Path path) {
    try {
        return Files.readString(path);
    } catch (IOException e) {
        throw new UncheckedIOException(e);
    }
}

list.stream()
    .map(this::readFileSafe)
    .toList();

Solution 2: Functional Wrapper

@FunctionalInterface
interface ThrowingFunction<T, R> {
    R apply(T t) throws Exception;
}

static <T, R> Function<T, R> unchecked(ThrowingFunction<T, R> f) {
    return t -> {
        try { return f.apply(t); }
        catch (Exception e) { throw new RuntimeException(e); }
    };
}

// Usage
list.stream()
    .map(unchecked(path -> Files.readString(path)))
    .toList();

Decision Flowchart

Is the condition recoverable by the caller?
├── YES → Can the caller realistically handle it?
│         ├── YES → Checked exception ✅
│         └── NO  → Unchecked exception
└── NO  → Is it a programming error?
          ├── YES → Unchecked exception (IllegalArgumentException, etc.)
          └── NO  → Error (OutOfMemoryError, etc.) — don't catch

Errors — The Third Category

Error subclasses represent JVM-level failures that applications generally should not catch:

Error Cause
OutOfMemoryError JVM ran out of heap space
StackOverflowError Infinite recursion
NoClassDefFoundError Class missing at runtime
// Generally DON'T do this
try {
    recursiveMethod();
} catch (StackOverflowError e) {
    // What can you even do here?
}

Common Mistakes

Mistake Problem Fix
Catching Exception broadly Hides bugs Catch specific types
throws Exception everywhere No useful information Declare specific exceptions
Empty catch for checked Swallows errors Log and/or rethrow
Making everything checked Verbose, annoying Use checked only for recoverable I/O
Catching Error Can’t recover from OOM Let the JVM handle it

Key Takeaways

  1. Checked = compiler-enforced, for recoverable conditions (IOException).
  2. Unchecked = not enforced, for programming errors (NullPointerException).
  3. Errors = JVM-level, don’t catch (OutOfMemoryError).
  4. Modern Java leans towards unchecked exceptions for cleaner code.
  5. When wrapping checked → unchecked, always preserve the cause.
  6. Use wrapper patterns for checked exceptions in streams and lambdas.

What’s Next?

Next: Custom Exceptions — creating domain-specific exceptions for your application.

Updated: