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
58: Exception Chaining
I want to show you a snippet of code that looks perfectly fine at first glance. It's a classic pattern: you have a high-level service that calls a low-level data access object. To keep your architecture clean, you don't want your business logic leaking database-specific exceptions, so you wrap them in a custom domain exception.
public class UserService {
private UserRepository repo = new UserRepository();
public void updateUserEmail(int userId, String email) {
try {
repo.updateEmail(userId, email);
} catch (SQLException e) {
// We don't want the UI layer dealing with SQL exceptions
throw new UserServiceException("Could not update user email in the system.");
}
}
}
The Mystery of the Vanishing Stack Trace
On the surface, this is "clean." The `UserServiceException` hides the implementation details of the database. But imagine this code is running in production and it fails. You check your logs, and all you see is:
com.app.UserServiceException: Could not update user email in the system.
That's it. No line numbers from the database driver, no "Connection Timed Out," and no "Unique Constraint Violation." You've effectively erased the evidence of what actually went wrong. By creating a new exception and ignoring the SQLException e, you've "swallowed" the root cause. I've spent hours of my life debugging issues exactly like this because some developer thought they were being helpful by simplifying the error message.
Preserving the Root Cause with Exception Chaining
The fix is simple, but it's a habit you need to build. Java's Throwable class (which all exceptions inherit from) has a constructor that accepts another Throwable as a "cause." This is called Exception Chaining.
Here is how we actually fix that UserService:
public class UserService {
private UserRepository repo = new UserRepository();
public void updateUserEmail(int userId, String email) {
try {
repo.updateEmail(userId, email);
} catch (SQLException e) {
// Pass 'e' as the second argument to preserve the chain
throw new UserServiceException("Could not update user email in the system.", e);
}
}
}
Now, for this to work, your custom exception needs to support that constructor. If you wrote your UserServiceException manually, make sure it looks like this:
public class UserServiceException extends RuntimeException {
public UserServiceException(String message) {
super(message);
}
public UserServiceException(String message, Throwable cause) {
super(message, cause);
}
}
Now, when that error hits your logs, you'll see the UserServiceException, followed by a "Caused by: java.sql.SQLException: ..." section. You get the best of both worlds: a high-level explanation for your application logic and the granular, technical detail required to actually fix the bug.
When to actually chain exceptions
You shouldn't chain every single exception just for the sake of it. If you're just catching an exception to re-throw the exact same type, chaining is redundant. Use it when you are translating an exception from one layer of your app to another. For example:
- Low-level
IOException→ High-levelConfigurationLoadException - Low-level
NoSuchElementException→ High-levelUserNotFoundException - Low-level
JsonParseException→ High-levelInvalidApiRequestException
It keeps your API clean while keeping your logs honest.
📋 Practical Task
Implement a Chained Exception for a Payment Gateway
You are building a payment processing module. You have a low-level BankApiConnectionException (which is a checked exception) that occurs when the external bank server is down. Your business logic requires a high-level PaymentProcessingException (a RuntimeException) to be thrown so the UI can display a generic error message.
Your Task:
- Create the
BankApiConnectionExceptionclass (extendsException). - Create the
PaymentProcessingExceptionclass (extendsRuntimeException) ensuring it has a constructor that accepts both aString messageand aThrowable cause. - Create a
PaymentServiceclass with a methodprocessPayment(). - Inside
processPayment(), simulate a failure by calling a private method that throwsBankApiConnectionException. - Catch that exception and throw a
PaymentProcessingException, properly chaining the original cause. - In your
mainmethod, wrap the call toprocessPayment()in a try-catch block and print the stack trace to verify that the "Caused by" section is present.
There are no comments for now.