Skip to Content
Course content
Click on the "Edit" button in the top corner of the screen to edit your slide content.

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-level ConfigurationLoadException
  • Low-level NoSuchElementException → High-level UserNotFoundException
  • Low-level JsonParseException → High-level InvalidApiRequestException

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:

  1. Create the BankApiConnectionException class (extends Exception).
  2. Create the PaymentProcessingException class (extends RuntimeException) ensuring it has a constructor that accepts both a String message and a Throwable cause.
  3. Create a PaymentService class with a method processPayment().
  4. Inside processPayment(), simulate a failure by calling a private method that throws BankApiConnectionException.
  5. Catch that exception and throw a PaymentProcessingException, properly chaining the original cause.
  6. In your main method, wrap the call to processPayment() in a try-catch block and print the stack trace to verify that the "Caused by" section is present.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.