If you ask three Java developers how to handle exceptions properly, you are likely to get four different answers. Few topics in the Java ecosystem spark as much debate—or as many "religious wars"—as the distinction between Checked and Unchecked exceptions. It is a feature almost unique to Java, and for decades, it has divided the community between those who value the compiler's strict enforcement and those who crave the flexibility of runtime behavior.
At a high level, the contenders are clear. Checked Exceptions are enforced at compile-time; if a method throws one, the compiler mandates that the caller must handle it or declare it. Unchecked Exceptions, on the other hand, allow for runtime flexibility, bubbling up the stack without mandating explicit try-catch blocks at every step.
However, memorizing the syntax is the easy part. The real challenge—and the purpose of this article—is to move beyond syntax and discuss the architectural philosophy of error handling. When should you force a catch block? When should you let an application crash? Let’s dissect the hierarchy, the philosophy, and the modern best practices for writing robust Java applications.

The Hierarchy: Understanding the Technical Distinction
To make informed architectural decisions, we first need to ground ourselves in the class hierarchy.
The Throwable Tree
At the very root of Java's error handling mechanism sits java.lang.Throwable. Everything that can be thrown or caught descends from this class. It splits immediately into two main branches:
- Error: These are serious problems that a reasonable application should not try to catch (e.g.,
OutOfMemoryError,StackOverflowError). We generally ignore these in application logic. - Exception: This is where our daily work lives. This branch is further split into Checked and Unchecked exceptions.
Checked Exceptions (The "Catch or Specify" Requirement)
Any class that extends java.lang.Exception—but strictly does not extend java.lang.RuntimeException—is a Checked Exception.
The intent behind Checked Exceptions (like IOException or SQLException) was noble: to create self-documenting code where failure modes are part of the API contract. The compiler enforces the "Catch or Specify" requirement. You must either wrap the code in a try-catch block or add throws to your method signature. This forces developers to acknowledge that specific external factors (like a missing file or a network timeout) will fail eventually.
Unchecked Exceptions (Runtime Faults)
Unchecked Exceptions are classes that extend java.lang.RuntimeException.
The original intent here was to signal programming errors or unrecoverable system states—things like NullPointerException, IllegalArgumentException, or IndexOutOfBoundsException. Because these often represent bugs in the code rather than environmental hazards, the compiler does not force you to catch them. If they occur, they usually imply that the application is in an invalid state.
The Philosophy: Contingency vs. Fault
The syntactic difference is binary, but the logical distinction requires nuance. A helpful mental model is distinguishing between a Contingency and a Fault.
When to Use Checked Exceptions
Use Checked Exceptions for Contingencies. A contingency is a foreseeable, recoverable event that is not a bug in the code.
For example, if you are writing a library to read a configuration file, a missing file is a contingency. It is not a bug in your parser; it is a valid environmental state. By throwing a checked FileNotFoundException, you force the calling code to decide: Should we create a default file? Should we prompt the user for a new path?
Rule of Thumb: If the caller can reasonably be expected to recover from the error, make it Checked.
When to Use Unchecked Exceptions
Use Unchecked Exceptions for Faults. A fault is a condition from which the caller cannot recover.
If your database is offline, or if a null value is passed where it strictly shouldn't be, the immediate calling method (e.g., a calculateTax() method) cannot fix the database or magically generate data. There is no recovery action other than aborting the operation. These errors should bubble up to a global exception handler that logs the error and sends an apology to the user.
The Cost of "Checked" Safety
While Checked Exceptions offer safety, they come with a high cost:
- Boilerplate: Developers often write empty catch blocks (
catch (Exception e) { /* ignore */ }) just to silence the compiler, which swallows errors and makes debugging impossible. - Signature Pollution: If a low-level repository method changes to throw a new Checked Exception, every method in the call stack above it must be updated to declare that exception, breaking encapsulation.
- Verbose Code: Business logic gets buried under layers of error handling infrastructure.
The Modern Shift: Why Frameworks Like Spring Prefer Runtime Exceptions
If you look at modern Java development, particularly within the Spring ecosystem, the tide has turned heavily in favor of Unchecked Exceptions.
The Spring Framework Approach
Consider the JDBC API. Standard JDBC throws SQLException (Checked) for everything—from a syntax error in your query to the database server being on fire.
Spring's JdbcTemplate catches these SQLExceptions and re-throws them as DataAccessException, which is a RuntimeException. Why? Because 99% of the time, you cannot handle a database failure inside a specific service method. Spring’s philosophy is that transaction rollback and error logging should happen at a higher, generic level, keeping your service layer clean.
Lambdas and Functional Programming
Java 8 introduced Streams and Lambdas, which brought the Checked Exception friction to a breaking point. Functional interfaces like Function or Consumer do not allow Checked Exceptions.
Consider this code:
// This fails to compile if doSomething throws a Checked Exception
list.stream().map(item -> doSomething(item)).collect(Collectors.toList());To make this work with Checked Exceptions, you have to wrap the lambda in a clumsy try-catch block, destroying the readability of the functional pipeline. Unchecked exceptions flow through streams seamlessly.
Clean Architecture Principles
Relying on Unchecked Exceptions aligns better with the Open/Closed Principle. When you introduce a new failure mode in a plugin or low-level module, you shouldn't have to modify the signatures of the high-level core interfaces that call it. Unchecked exceptions allow errors to tunnel through the architecture without forcing the intermediate layers to be aware of them.
Best Practices for Your Codebase
Based on modern architectural standards, here is how you should handle exceptions in your projects today.
1. Default to Unchecked for Custom Exceptions
When creating custom domain exceptions (e.g., UserNotFoundException, PaymentFailedException), default to extending RuntimeException.
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}Only extend Exception (Checked) if you have a specific requirement where you must force the API consumer to write a catch block for recovery logic.
2. Exception Translation (Wrapping)
Do not let low-level checked exceptions leak into your business logic. Catch them at the architectural boundary (like the Adapter or Repository layer) and rethrow them as meaningful unchecked exceptions.
public void readFile(String path) {
try {
// Low-level logic
Files.readAllLines(Paths.get(path));
} catch (IOException e) {
// Wrap and Rethrow
throw new StorageAccessException("Failed to read configuration", e);
}
}Notice we passed e as the cause. Always preserve the stack trace.
3. Avoid Catching "Exception" or "Throwable"
Never write this:
catch (Exception e) {
log.error("Error");
}This "catch-all" swallows runtime bugs (like NullPointerException) that you didn't intend to handle, making them indistinguishable from expected errors. Catch specific exceptions, or catch nothing and let it bubble.
4. Document Your Runtime Exceptions
Just because the compiler doesn't force you to catch them doesn't mean you shouldn't document them. Use Javadoc @throws tags to inform other developers about potential runtime failures.
/**
* @throws IllegalArgumentException if the email format is invalid
*/
public void registerUser(String email) { ... }Conclusion
The debate between Checked and Unchecked exceptions is ultimately a balance between compiler safety and code maintainability. While Checked Exceptions have a valid place for recoverable contingencies, the modern consensus—championed by frameworks like Spring and Clean Architecture proponents—heavily favors Unchecked Exceptions for the vast majority of application logic.
By wrapping library-specific checked exceptions into runtime exceptions and letting unrecoverable errors bubble up to a global handler (like a Spring @ControllerAdvice), you keep your business logic clean, readable, and focused on success paths rather than failure plumbing.
Building secure, privacy-first tools means staying ahead of security threats. At ToolShelf, all operations happen locally in your browser—your data never leaves your device, providing security through isolation.
Stay secure & happy coding,
— ToolShelf Team