Java Checked Exceptions

Su Suriya Ravichandran Updated 14 Nov 2026
10 min read ·Lesson 129 of 133

What Makes an Exception "Checked"

A checked exception is an exception that the Java compiler checks at compile time. If a method can throw one, the code must deal with it — by catching it or by declaring it with throws — or the program doesn't compile.

Technically, an exception is checked when its class is a subclass of Exception but not a subclass of RuntimeException. Everything under RuntimeException, and everything under Error, is unchecked.

Throwable
 ├── Error                     → unchecked
 └── Exception                 → checked
      ├── IOException          → checked
      ├── SQLException         → checked
      ├── InterruptedException → checked
      └── RuntimeException     → unchecked (and all its subclasses)

"Checked" describes a compile-time rule only. At runtime the JVM treats checked and unchecked exceptions exactly the same way: both are thrown, propagate up the call stack and can be caught by try...catch. The difference is whether the compiler forces you to acknowledge them.

You can confirm which category a class belongs to by checking its ancestors:

import java.util.*;

public class Kinds {
    static String kind(Class<? extends Throwable> t) {
        if (RuntimeException.class.isAssignableFrom(t) || Error.class.isAssignableFrom(t)) return "unchecked";
        return "checked";
    }

    public static void main(String[] args) {
        List<Class<? extends Throwable>> types = List.of(
            java.io.IOException.class, java.io.FileNotFoundException.class, java.sql.SQLException.class,
            InterruptedException.class, ClassNotFoundException.class, java.text.ParseException.class,
            java.net.URISyntaxException.class, java.util.concurrent.TimeoutException.class,
            java.util.concurrent.ExecutionException.class, CloneNotSupportedException.class,
            Exception.class, NumberFormatException.class, StackOverflowError.class);
        for (var t : types) System.out.printf("%-28s %s%n", t.getSimpleName(), kind(t));
    }
}

Output:

IOException                  checked
FileNotFoundException        checked
SQLException                 checked
InterruptedException         checked
ClassNotFoundException       checked
ParseException               checked
URISyntaxException           checked
TimeoutException             checked
ExecutionException           checked
CloneNotSupportedException   checked
Exception                    checked
NumberFormatException        unchecked
StackOverflowError           unchecked

A.isAssignableFrom(B) returns true when B is A or a subclass of A. So the method reports "unchecked" for anything below RuntimeException or Error, and "checked" for everything else.

Why Java Has Checked Exceptions

Checked exceptions model failures that are outside the program's control but entirely foreseeable:

  • A file can be deleted, locked or on a full disk (IOException).
  • A database can be down or reject a query (SQLException).
  • A network call can time out (TimeoutException, IOException).
  • A thread can be asked to stop while it waits (InterruptedException).
  • User-supplied text can be an invalid URL or date (URISyntaxException, ParseException).

No amount of careful coding prevents these situations; a correct program still has to decide what happens when they occur. Java's designers made such failures part of a method's signature so that callers can't overlook them. Compare this with unchecked exceptions, which usually signal bugs that the right fix is to correct, not to handle (covered in the next lesson).

The Catch-or-Declare Rule

For every checked exception that can come out of a statement, the enclosing code must do one of two things:

  1. Catch it with a try...catch whose type matches it or a superclass.
  2. Declare it in the method's throws clause, passing responsibility to the caller.
// Option 1: catch
String loadOrDefault(Path p) {
    try {
        return Files.readString(p);
    } catch (IOException e) {
        return "";
    }
}

// Option 2: declare
String load(Path p) throws IOException {
    return Files.readString(p);
}

Leaving out both gives the compile error unreported exception IOException; must be caught or declared to be thrown, as shown in the previous lessons.

The compiler knows which checked exceptions a call can produce from the throws clause of the called method (or constructor). It doesn't analyse whether the exception will really happen at runtime — Files.readString declares IOException, so every call must account for it, even for a file you just created.

Catching a Checked Exception That Can't Occur

The compiler also works in the other direction. A catch for a checked exception that the try block can never throw is an error:

import java.io.IOException;

public class Never {
    public static void main(String[] args) {
        try {
            System.out.println("Hello");
        } catch (IOException e) {
            System.out.println("cannot happen");
        }
    }
}

Compiler output:

Never.java:7: error: exception IOException is never thrown in body of corresponding try statement
        } catch (IOException e) {
          ^
1 error

This protects you from dead handlers — typically left behind after refactoring removed the code that used to throw. Two types are exempt: catch (Exception e) and catch (Throwable e) always compile, because they also cover unchecked exceptions, which any code may throw.

Common Checked Exceptions and What They Mean

ExceptionPackageThrown whenTypical response
IOExceptionjava.ioAny input/output failureReport, retry, use a fallback
FileNotFoundException, NoSuchFileExceptionjava.io, java.nio.fileA file doesn't exist or can't be openedAsk for another path, create defaults
SQLExceptionjava.sqlA database operation failsRoll back, retry, report
InterruptedExceptionjava.langA waiting/sleeping thread is interruptedStop the work and restore the interrupt flag
TimeoutExceptionjava.util.concurrentA blocking call exceeds its time limitRetry, cancel, return a partial result
ExecutionExceptionjava.util.concurrentA task run in another thread failedInspect getCause()
URISyntaxExceptionjava.netA string isn't a valid URIReject the input
ParseExceptionjava.textLegacy DateFormat/NumberFormat parsing failsReject the input
ClassNotFoundExceptionjava.langLoading a class by name failsFail with a configuration error
CloneNotSupportedExceptionjava.langclone() called on a class that isn't CloneableImplement Cloneable or use a copy constructor

Checked and Unchecked Versions of the Same Operation

Some JDK APIs offer both styles, which shows the design idea clearly. A URI typed by a user may be invalid, so the constructor declares a checked exception. A URI written as a constant in your own source code should always be valid, so URI.create throws an unchecked one instead:

import java.net.*;

public class Uri {
    static URI parse(String text) {
        try {
            return new URI(text);
        } catch (URISyntaxException e) {
            System.out.println("rejected: " + e.getMessage() + " (index " + e.getIndex() + ")");
            return null;
        }
    }

    public static void main(String[] args) {
        System.out.println(parse("https://gocourse.in/java?page=2").getHost());
        parse("https://gocourse.in/java tutorial");
        System.out.println(URI.create("https://example.com/a").getPath());
        try {
            URI.create("https://example.com/a b");
        } catch (IllegalArgumentException e) {
            System.out.println(e.getClass().getSimpleName() + ": " + e.getMessage());
        }
    }
}

Output:

gocourse.in
rejected: Illegal character in path at index 24: https://gocourse.in/java tutorial (index 24)
/a
IllegalArgumentException: Illegal character in path at index 21: https://example.com/a b

How it works:

  • new URI(text) declares throws URISyntaxException, so parse must catch it. URISyntaxException also offers extra information: getIndex() returns the position of the bad character (the space at index 24), which is useful for highlighting the error in a form.
  • URI.create(text) wraps the same check but throws IllegalArgumentException (unchecked). It's meant for values known at compile time, such as URI.create("https://api.example.com") in a configuration class, where an invalid value is a bug in your code, not a runtime condition.

The same principle guides your own designs: if the caller can reasonably cause or recover from the failure at runtime, a checked exception documents that; if the failure means the program itself is wrong, an unchecked exception fits better.

Handling InterruptedException Correctly

InterruptedException is the checked exception developers most often handle incorrectly. It is thrown by blocking methods such as Thread.sleep, Object.wait, Thread.join and BlockingQueue.take when another thread calls interrupt() on the waiting thread. Interruption is Java's cooperative way of asking a thread to stop what it's doing — used, for example, when an application shuts down or a task is cancelled.

public class Interrupt {
    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            try {
                System.out.println("worker: polling for new orders");
                Thread.sleep(10_000);
                System.out.println("worker: woke up normally");
            } catch (InterruptedException e) {
                System.out.println("worker: interrupted, flag now = " + Thread.currentThread().isInterrupted());
                Thread.currentThread().interrupt();
                System.out.println("worker: flag restored = " + Thread.currentThread().isInterrupted());
            }
        });
        worker.start();
        Thread.sleep(200);
        worker.interrupt();
        worker.join();
        System.out.println("main: worker finished");
    }
}

Output:

worker: polling for new orders
worker: interrupted, flag now = false
worker: flag restored = true
main: worker finished

What happens:

  1. The worker starts a 10-second sleep. After 200 ms, main calls worker.interrupt().
  2. Thread.sleep wakes up early by throwing InterruptedException. "woke up normally" is never printed.
  3. When it throws, the JVM clears the thread's interrupted flag — the first isInterrupted() prints false. If the handler just ignored the exception, the request to stop would be lost, and code further up the stack (a thread pool, a loop checking isInterrupted()) would never know the thread was asked to stop.
  4. Thread.currentThread().interrupt() sets the flag again, so the cancellation request is preserved.

The rule: when you catch InterruptedException, either declare it (throws InterruptedException) so callers see it, or restore the flag and stop the current work. An empty catch (InterruptedException e) {} is a well-known source of applications that refuse to shut down.

Practical Example: Retrying a Remote Call

Network calls fail temporarily all the time — a connection resets, a server restarts. Because such failures are checked IOExceptions, the compiler makes sure the calling code thinks about them. A common response is to retry a few times with an increasing delay (exponential backoff).

import java.io.IOException;
import java.time.*;

public class Retry {
    static int calls = 0;

    // Simulates a remote currency service that fails twice, then answers
    static double fetchUsdToInr() throws IOException {
        calls++;
        if (calls < 3) {
            throw new IOException("connection reset (attempt " + calls + ")");
        }
        return 83.42;
    }

    static double fetchWithRetry(int maxAttempts, Duration firstDelay) throws IOException, InterruptedException {
        Duration delay = firstDelay;
        for (int attempt = 1; ; attempt++) {
            try {
                return fetchUsdToInr();
            } catch (IOException e) {
                if (attempt == maxAttempts) {
                    throw e;
                }
                System.out.println("  " + e.getMessage() + ", retrying in " + delay.toMillis() + " ms");
                Thread.sleep(delay.toMillis());
                delay = delay.multipliedBy(2);
            }
        }
    }

    public static void main(String[] args) {
        try {
            System.out.println("rate = " + fetchWithRetry(5, Duration.ofMillis(100)));
            calls = 0;
            System.out.println("rate = " + fetchWithRetry(2, Duration.ofMillis(100)));
        } catch (IOException e) {
            System.out.println("giving up: " + e.getMessage());
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.out.println("cancelled");
        }
    }
}

Output:

  connection reset (attempt 1), retrying in 100 ms
  connection reset (attempt 2), retrying in 200 ms
rate = 83.42
  connection reset (attempt 1), retrying in 100 ms
giving up: connection reset (attempt 2)

Step by step:

  1. fetchUsdToInr declares throws IOException. Every caller is forced by the compiler to plan for network failure. The simulation fails on the first two calls and succeeds on the third.
  2. The retry loop catches IOException. Each failure is printed, the thread sleeps, and the delay doubles (100 ms, 200 ms, 400 ms, ...). Doubling gives a struggling server progressively more time to recover instead of hammering it.
  3. On the last allowed attempt, the exception is rethrown with throw e. The method can no longer recover, so it reports the original failure to its caller unchanged. With five attempts allowed, the first call succeeds on attempt 3; with two allowed, the second call gives up after attempt 2.
  4. fetchWithRetry declares two checked exceptions. IOException comes from the rethrow; InterruptedException comes from Thread.sleep. It deliberately does not catch InterruptedException: if the application is shutting down during a backoff, retrying further would be wrong, so the request to stop is passed up immediately.
  5. main is the boundary that handles everything. It reports the network failure, and for interruption it restores the flag before giving up.

Production systems usually get this behaviour from a library (such as Resilience4j or Spring Retry) and add random jitter to the delay so that many clients don't retry at the same instant, but the exception handling underneath follows the same structure.

Limitations and Criticism of Checked Exceptions

Checked exceptions are a Java-specific feature; C#, Kotlin, Scala and most other languages don't have them. The common criticisms are worth knowing because they explain patterns you'll see in modern code:

  • They don't work with lambdas and streams. Standard functional interfaces declare no checked exceptions, so code like paths.stream().map(Files::readString) doesn't compile. Code wraps them in unchecked exceptions such as UncheckedIOException (shown in the Throws Keyword lesson).
  • They leak implementation details through layers. If a repository method declares throws SQLException, every service and controller above it must mention SQL even though it has nothing to do with databases. The usual solution is to translate the low-level exception into an application exception at the layer boundary (Exception Chaining lesson).
  • They encourage empty catch blocks when developers just want the code to compile — the worst possible outcome.

Because of this, many modern frameworks prefer unchecked exceptions: Spring translates SQLException into its unchecked DataAccessException hierarchy, and JPA/Hibernate throw unchecked PersistenceExceptions. Checked exceptions remain standard in the JDK's I/O, networking and concurrency APIs, so every Java developer needs to handle them confidently.

0 Comments

Reviewed before they appear

No comments yet.

Java
Java Introduction Java Features Java Syntax Java Comments Java Statements Java Output Java Variables Java Data Types Java Type Casting Java Operators Java Strings Java Numbers Java Math Java Booleans Java User Input Java Conditions Java If Else Java Switch Java While Loop Java For Loop Java Break Java Continue Java Arrays Java Methods Java Parameters Java Return Values Java Scope Java Recursion Install Java Install JDK Java IDEs Java JDK Java JRE Java JVM Java Compiler Java File Compile Java Run Java JShell Basics Java Packages Java OOP Java Classes Java Objects Class Methods Constructors Constructor Overload this Keyword static Keyword final Keyword Encapsulation Inheritance Polymorphism Abstraction Access Modifiers super Keyword Method Overload Method Override Instanceof Operator Nested Classes Inner Classes Anonymous Classes Enums Records Array Basics Array Length Loop Arrays Multidimensional Arrays Array Copy Array Sorting Array Search ArrayList LinkedList HashSet TreeSet HashMap TreeMap Queue Interface Deque Interface Stack Class Iterator List Interface Set Interface Map Interface Collections Class Comparable Comparator Generics Basics Generic Classes Generic Methods Wildcards String Basics String Length String Methods String Concatenation String Compare String Search String Replace String Split String Format StringBuilder StringBuffer Text Blocks Regular Expressions Number Types Integer Class Float Class Double Class BigInteger BigDecimal Math Class Random Numbers Number Format Date Basics LocalDate LocalTime LocalDateTime ZonedDateTime Date Formatting Date Parsing Period Class Duration Class Java Instant Class Java Exception Basics Java try...catch Java Multiple Catch Blocks Java finally Block Java throw Keyword Java throws Keyword Java Checked Exceptions Java Unchecked Exceptions Java Custom Exceptions Java try-with-resources Java Exception Chaining
Ask about this post
AI Ask about this post

Ask questions about Java Checked Exceptions and get answers drawn from it.

Signed-in readers only.