What is a checked exception and when to use it
A checked exception is an exceptional condition in Java that the javac compiler mandatorily requires to be handled or declared before the code can compile.
🟢 Junior Level
A checked exception is an exceptional condition in Java that the javac compiler mandatorily requires to be handled or declared before the code can compile.
In the class hierarchy, checked exceptions comprise all subclasses of java.lang.Exception, excluding the java.lang.RuntimeException subtree.
java.lang.Throwable
│
┌────────────────┴────────────────┐
▼ ▼
java.lang.Error java.lang.Exception
(Unchecked) (Checked by default)
│
▼
java.lang.RuntimeException
(Unchecked)
The Design Philosophy (James Gosling’s Vision)
When Java was conceived, the designers prioritized software robustness and reliability. When a program interacts with external environments (file systems, networks, hardware, or databases), failures are inevitable due to physical constraints outside the program’s control (e.g., full disk, severed cable, missing file, or DNS failure).
Checked exceptions force developers at compile time to make an explicit architectural decision:
- Handle and recover locally: Enclose the operation within a
try-catchblock. - Propagate responsibility upward: Warn callers explicitly in the method signature using
throws IOException.
Basic Code Example
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class CheckedBasics {
// Approach 1: Delegate responsibility to the caller via throws
public String loadData(Path path) throws IOException {
return Files.readString(path);
}
// Approach 2: Recover locally via try-catch
public String loadDataWithFallback(Path path) {
try {
return Files.readString(path);
} catch (IOException e) {
// Graceful recovery: return cached or default configuration
return "DEFAULT_CONFIG_DATA";
}
}
}
The 3 Canonical Criteria for Using Checked Exceptions (Joshua Bloch)
According to Joshua Bloch (Effective Java), checked exceptions are justified only when all three conditions are satisfied:
- The failure is anticipated and environmental: It is not a software bug or contract violation, but an expected external reality.
- The failure is programmatically recoverable: The calling code can take a meaningful, automated recovery action (retry with backoff, switch to a backup server, fall back to cached data).
- The caller is strictly required to react: Ignoring the situation would compromise business logic integrity or leave the system in an inconsistent state.
🟡 Middle Level
Method Overriding and the Liskov Substitution Principle (LSP)
When overriding (@Override) methods in subclasses or implementing interface contracts, the Java compiler enforces strict rules on checked exception declarations according to the Liskov Substitution Principle (LSP):
public class ParentService {
public void execute() throws IOException { /* ... */ }
}
public class ChildService extends ParentService {
// 1. ALLOWED: Declare a narrower checked exception (subtype of IOException)
@Override
public void execute() throws FileNotFoundException { /* ... */ }
// 2. ALLOWED: Omit checked exceptions entirely (subclass method is strictly safer)
// @Override
// public void execute() { /* ... */ }
// 3. ALLOWED: Declare ANY unchecked exception (RuntimeException)
// @Override
// public void execute() throws IllegalArgumentException { /* ... */ }
// 4. FORBIDDEN: Declare broader checked exception (Exception) or brand new checked (SQLException)
// @Override
// public void execute() throws Exception { } // COMPILATION ERROR!
}
Why This Is Enforced
Polymorphic client code interacts with references of the parent type:
ParentService service = new ChildService();
try {
service.execute();
} catch (IOException e) {
// Client only handles IOException!
}
If ChildService were permitted to throw SQLException, the client’s catch (IOException e) block would fail to intercept it, crashing the thread and violating type-safe substitution.
The Canonical Handling of InterruptedException
InterruptedException is one of the few well-designed checked exceptions in the standard library. It signals that an external thread requested cancellation via Thread.interrupt().
The Lethal Anti-Pattern: Swallowing InterruptedException
// NEVER DO THIS:
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// Empty catch or e.printStackTrace():
// The thread's interrupted status flag has been CLEARED to false!
}
When InterruptedException is thrown, the JVM clears the interrupted status flag of the current thread. Swallowing it prevents thread pools (e.g., ThreadPoolExecutor) and schedulers from knowing that the thread has been cancelled, causing tasks to hang and preventing clean application shutdown.
The Senior Solution: Restore or Rethrow
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// Restore interrupted status for upstream frameworks/pools
Thread.currentThread().interrupt();
log.warn("Task execution interrupted on thread {}", Thread.currentThread().getName(), e);
throw new ProcessAbortedException("Execution aborted due to thread interrupt", e);
}
🔴 Senior Level
Binary & Source Compatibility (JLS §13.4.21)
In enterprise library and SDK design, modifying method signatures carries significant semantic impact:
- Source Incompatibility: Adding a checked exception (
throws IOException) to an existing public interface method is a major breaking change. Any downstream project updating the dependency will immediately fail compilation (unhandled exception). - Binary Compatibility: In contrast, adding an unchecked exception (
RuntimeException) preserves both source and binary compatibility without forcing client recompilation.
Modern Architectural Alternatives to Checked Exceptions
Due to signature pollution and friction with functional programming, modern production Java relies on cleaner alternatives:
1. Optional<T> for Legitimate Absence
If a requested entity might naturally not exist, absence is an expected state rather than an exceptional failure:
// Anti-pattern: User findById(Long id) throws UserNotFoundException;
// Modern pattern:
public Optional<User> findById(Long id);
2. Sealed Result<T> Interface (Either / Result Pattern)
Using Java 21 sealed interfaces and pattern matching, expected failures can be modeled as typed domain values without the heavy CPU overhead of stack trace generation:
package com.example.result;
public sealed interface Result<T> {
record Success<T>(T value) implements Result<T> {}
record Failure<T>(String errorCode, String message) implements Result<T> {}
}
Service implementation:
public Result<PaymentReceipt> processPayment(PaymentRequest request) {
if (!accountService.hasFunds(request.accountId(), request.amount())) {
return new Result.Failure<>("INSUFFICIENT_FUNDS", "Account balance is too low");
}
PaymentReceipt receipt = paymentGateway.charge(request);
return new Result.Success<>(receipt);
}
Caller evaluation using pattern matching:
Result<PaymentReceipt> outcome = paymentService.processPayment(request);
return switch (outcome) {
case Result.Success<PaymentReceipt> s -> ResponseEntity.ok(s.value());
case Result.Failure<PaymentReceipt> f -> ResponseEntity.badRequest().body(f.message());
};
3. Functional Adapters for Stream API
To seamlessly process legacy methods throwing checked exceptions inside Stream pipelines, production systems implement generic function wrappers:
@FunctionalInterface
public interface ThrowingFunction<T, R, E extends Exception> {
R apply(T t) throws E;
static <T, R> java.util.function.Function<T, R> unchecked(ThrowingFunction<T, R, ?> f) {
return t -> {
try {
return f.apply(t);
} catch (Exception ex) {
if (ex instanceof RuntimeException runtimeException) {
throw runtimeException;
}
throw new RuntimeException("Checked exception during stream evaluation", ex);
}
};
}
}
Usage:
List<String> content = paths.stream()
.map(ThrowingFunction.unchecked(Files::readString))
.toList();
4 Tricky Questions
1. Can a subclass method declare a new checked exception when overriding a parent method?
Answer:
No. Under the Liskov Substitution Principle (LSP) and the Java Language Specification (JLS §8.4.8.3), an overriding method cannot declare any new or broader checked exceptions than those declared by the superclass method. It may declare fewer checked exceptions, narrower subtypes, or no checked exceptions at all. However, it is free to declare any unchecked exceptions (RuntimeException).
2. What happens to binary and source compatibility if you add throws IOException to an existing public interface method?
Answer:
Source compatibility is broken immediately: all client code that invokes this method without an enclosing try-catch(IOException) will fail to compile. Adding checked exceptions to published public APIs is considered a catastrophic breaking change in library versioning.
3. Why is InterruptedException a checked exception in Java, and how must it be handled?
Answer:
It was made a checked exception to prevent developers from casually ignoring thread cancellation requests. When caught, the JVM clears the thread’s interrupted status flag to false. Handlers must never swallow the exception; they must either rethrow it or restore the interrupted flag by calling Thread.currentThread().interrupt() so that outer schedulers and thread pools can safely terminate execution.
4. How does handling errors via the Result<T> pattern compare to checked exceptions regarding performance?
Answer:
Instantiating an exception is expensive because Throwable invokes the native method fillInStackTrace(), forcing the JVM to inspect and record execution call frames from the thread stack. In contrast, the Result<T> record is a lightweight, stackless object allocated rapidly in the JVM Eden space without native stack traversal, executing 30x–50x faster in high-throughput workloads.
🎯 Interview Cheat Sheet
30-Second Summary
A checked exception is any subclass of
java.lang.Exception(excludingRuntimeException) that thejavaccompiler requires to be caught or declared viathrows.Per Effective Java, checked exceptions should only be used when a failure is external, anticipated, and programmatically recoverable by the caller. If recovery is impossible, checked exceptions become an anti-pattern causing signature pollution and breaking compatibility with Java 8 Streams and lambdas.
Modern enterprise applications favor unchecked domain exceptions,
Optional<T>, and sealedResult<T>types.
LSP Method Overriding Summary
- Narrower subtype checked exception: ✅ Allowed
- Omit checked exception entirely: ✅ Allowed
- Any unchecked exception (
RuntimeException): ✅ Allowed - Broader or new checked exception: ❌ Compilation Error
Red Flags (What to Avoid)
- ❌ “I add
throws Exceptionto every method so the compiler stops bothering me.” (Completely destroys type safety and pollutes client contracts). - ❌ “Swallowing checked exceptions with an empty
catchblock is fine if you don’t know how to handle them.” (Creates silent bugs that are nearly impossible to diagnose in production). - ❌ “Subclasses can declare any checked exception they need.” (Violates Liskov Substitution Principle; code will not compile).
- ❌ “InterruptedException can be logged and ignored in a loop.” (Clears the cancellation flag, preventing thread pools from shutting down).
Related Topics
- What is the difference between checked and unchecked exceptions — Comparison and runtime bytecode analysis
- What is an unchecked exception (Runtime Exception) — Unchecked exceptions in modern architecture
- Which exceptions must be handled — Handling requirements
- What does the throws keyword do — Method signature mechanics
- What are suppressed exceptions — Suppressed exceptions in try-with-resources