Java Checked Exceptions
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:
- Catch it with a
try...catchwhose type matches it or a superclass. - Declare it in the method's
throwsclause, 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
| Exception | Package | Thrown when | Typical response |
|---|---|---|---|
IOException | java.io | Any input/output failure | Report, retry, use a fallback |
FileNotFoundException, NoSuchFileException | java.io, java.nio.file | A file doesn't exist or can't be opened | Ask for another path, create defaults |
SQLException | java.sql | A database operation fails | Roll back, retry, report |
InterruptedException | java.lang | A waiting/sleeping thread is interrupted | Stop the work and restore the interrupt flag |
TimeoutException | java.util.concurrent | A blocking call exceeds its time limit | Retry, cancel, return a partial result |
ExecutionException | java.util.concurrent | A task run in another thread failed | Inspect getCause() |
URISyntaxException | java.net | A string isn't a valid URI | Reject the input |
ParseException | java.text | Legacy DateFormat/NumberFormat parsing fails | Reject the input |
ClassNotFoundException | java.lang | Loading a class by name fails | Fail with a configuration error |
CloneNotSupportedException | java.lang | clone() called on a class that isn't Cloneable | Implement 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)declaresthrows URISyntaxException, soparsemust catch it.URISyntaxExceptionalso 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 throwsIllegalArgumentException(unchecked). It's meant for values known at compile time, such asURI.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:
- The worker starts a 10-second sleep. After 200 ms,
maincallsworker.interrupt(). Thread.sleepwakes up early by throwingInterruptedException."woke up normally"is never printed.- When it throws, the JVM clears the thread's interrupted flag — the first
isInterrupted()printsfalse. 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 checkingisInterrupted()) would never know the thread was asked to stop. 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:
fetchUsdToInrdeclaresthrows 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.- 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. - 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. fetchWithRetrydeclares two checked exceptions.IOExceptioncomes from the rethrow;InterruptedExceptioncomes fromThread.sleep. It deliberately does not catchInterruptedException: if the application is shutting down during a backoff, retrying further would be wrong, so the request to stop is passed up immediately.mainis 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 asUncheckedIOException(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.