Checked vs Unchecked Exceptions in Java
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 checkArrayIndexOutOfBoundsException→ fix the index logicIllegalArgumentException→ 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
- Self-documenting — the method signature tells you what can go wrong.
- Compiler safety net — you can’t forget to handle a known failure.
- Forced consideration — you must think about error recovery.
Arguments AGAINST Checked Exceptions
- Verbose — leads to boilerplate
try-catchblocks everywhere. - Leaky abstractions —
throws SQLExceptionin a service layer exposes implementation. - Stream-hostile — lambdas and streams don’t play well with checked exceptions.
- 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);
Sneaky Throws (Advanced — Not Recommended)
@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
- Checked = compiler-enforced, for recoverable conditions (
IOException). - Unchecked = not enforced, for programming errors (
NullPointerException). - Errors = JVM-level, don’t catch (
OutOfMemoryError). - Modern Java leans towards unchecked exceptions for cleaner code.
- When wrapping checked → unchecked, always preserve the cause.
- Use wrapper patterns for checked exceptions in streams and lambdas.
What’s Next?
Next: Custom Exceptions — creating domain-specific exceptions for your application.