Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
53: Checked vs Unchecked Exceptions
I've seen a lot of developers enter Java with a mindset that "more safety is always better." When they first encounter checked exceptions, it seems like a superpower. The compiler basically forces you to think about what could go wrong. But if you apply that logic blindly, you end up creating a codebase that is practically impossible to maintain. Let's look at where this goes wrong and how we actually handle this in a professional production environment.
The Trap of the "Throws" Chain
Imagine we're building a payment processing system. A naive approach is to create a custom checked exception for everything that isn't a "success." You might create a PaymentException that extends Exception, and use it for every possible failure—from a declined credit card to a database timeout.
public class PaymentService {
public void processPayment(PaymentRequest request) throws PaymentException {
if (request.getAmount() <= 0) {
throw new PaymentException("Invalid amount");
}
// Imagine a network call here
if (!gateway.isAvailable()) {
throw new PaymentException("Gateway down");
}
}
}
At first, this looks responsible. But here is where the "exception pollution" starts. Because PaymentException is checked, every single method that calls processPayment must now also declare throws PaymentException or wrap the call in a try-catch block. If your payment logic is buried five layers deep in your service layer, you end up with five different methods all declaring throws PaymentException just to pass the buck upward. It's a leaky abstraction; your high-level business logic suddenly knows far too much about the low-level failures of the payment gateway.
When this happens, developers get tired of the boilerplate. They start doing the one thing you should never do: the empty catch block. You'll see catch (PaymentException e) { /* TODO: fix this later */ } scattered everywhere. Now you've lost all the safety the checked exception was supposed to provide, and you've added a massive amount of noise to your code.
Drawing the Line Between Bugs and Failures
The key to getting this right is asking yourself one question: Can the caller actually do anything meaningful to recover from this?
If the answer is "no," use an unchecked exception (extend RuntimeException). If the answer is "yes," a checked exception is a valid tool. I generally split my exceptions into two camps: programming errors and environmental failures.
Take that PaymentRequest example again. If the amount is less than or equal to zero, that's not a "failure" in the sense that the network went down; it's a bug. The developer who called the method passed invalid data. They shouldn't be "forced" to catch that—they should be forced to fix the bug. For this, I'd use an IllegalArgumentException, which is unchecked.
public void processPayment(PaymentRequest request) {
if (request.getAmount() <= 0) {
// This is a developer error. No need to force a try-catch.
throw new IllegalArgumentException("Amount must be positive");
}
try {
gateway.charge(request);
} catch (GatewayTimeoutException e) {
// This is an environmental failure. The caller MUST decide
// if they want to retry or tell the user to try again later.
throw new PaymentRetryableException("Gateway timed out", e);
}
}
In this version, the IllegalArgumentException doesn't clutter the method signatures. It just crashes the thread (or is caught by a global error handler) because it's a logic error. Meanwhile, the PaymentRetryableException (which we'll make a checked exception) signals to the UI layer that it's time to show a "Retry" button to the user.
The Cost of the Choice
The trade-off here is between strictness and ergonomics. Checked exceptions provide a strict contract, but they are brittle. If you add a new checked exception to a method in a shared library, you've just broken every single piece of code that calls that method across your entire organization.
That's why modern Java frameworks—and languages like Kotlin or Scala—have moved almost entirely toward unchecked exceptions. I personally lean toward RuntimeException for 90% of my cases. I only reach for a checked exception when the failure is a common, expected part of the business flow that the calling code absolutely must handle to maintain system integrity. If you're unsure, go with unchecked. It's much easier to make an exception checked later than it is to remove a checked exception from a public API once a hundred people are using it.
📋 Practical Task
Refactoring the UserAccountManager Exception Hierarchy
You have inherited a piece of code for a UserAccountManager. Currently, it uses a single checked exception AccountException for everything, which has led to a mess of empty catch blocks in the UI layer. Your task is to refactor the exception handling to distinguish between programming errors (which should be unchecked) and recoverable business failures (which should remain checked).
The Requirements:
- Identify the "Developer Error" (e.g., passing a null username) and change it to throw a
NullPointerExceptionorIllegalArgumentException. - Identify the "Recoverable Failure" (e.g., the account is locked due to too many password attempts) and keep it as a checked exception (e.g.,
AccountLockedException). - Remove the unnecessary
throws AccountExceptionfrom the method signatures where you have implemented unchecked exceptions.
// STARTING CODE
public class AccountException extends Exception {
public AccountException(String msg) { super(msg); }
}
public class UserAccountManager {
public void updatePassword(String username, String newPassword) throws AccountException {
if (username == null) {
throw new AccountException("Username cannot be null");
}
if (isAccountLocked(username)) {
throw new AccountException("Account is locked");
}
// update logic here...
}
private boolean isAccountLocked(String username) {
return true; // Mock implementation
}
}
There are no comments for now.