Rethrow a SQLException as a domain exception without losing the original cause
The repository below lets a raw SQLException escape to its caller. Wrap it in the domain type UserLoadException so callers see one vocabulary — without throwing away the original failure.
Constraints:
- the original
SQLExceptionmust stay reachable throughgetCause() - the printed stack trace must show it under
Caused by: UserLoadExceptionis unchecked, andfindUsermust not declarethrows
class UserLoadException extends RuntimeException {
// your code here
}
User findUser(long id) {
try {
return jdbc.queryForUser(id); // throws SQLException
} catch (SQLException e) {
// your code here
}
}
Write the implementation.
Give the custom type a (String, Throwable) constructor that forwards to super(message, cause), then rethrow with throw new UserLoadException(message, e);. Throwable itself stores that cause, so getCause() returns the SQLException and printStackTrace prints its frames under Caused by:. Passing only e.getMessage() into a message-only constructor keeps the text and silently discards the original stack trace.
- ✗Rethrowing with
e.getMessage()only, which drops the original stack trace - ✗Assuming the cause is linked automatically because the throw sits inside a
catch - ✗Confusing
addSuppressedwithinitCause— suppression is not chaining
- →When would you use
initCause()instead of a constructor taking the cause? - →How deep can a chain of causes go, and how do you walk it programmatically?
Solution
class UserLoadException extends RuntimeException {
UserLoadException(String message, Throwable cause) {
super(message, cause);
}
}
User findUser(long id) {
try {
return jdbc.queryForUser(id);
} catch (SQLException e) {
throw new UserLoadException("cannot load user " + id, e);
}
}
What super(message, cause) actually does. The cause field lives on Throwable itself, not on your class. The Throwable(String, Throwable) constructor records it once; from then on getCause() returns the original SQLException, and printStackTrace prints its frames as a separate section:
UserLoadException: cannot load user 42
at UserRepository.findUser(UserRepository.java:17)
...
Caused by: java.sql.SQLException: connection reset
at com.mysql.cj.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:...)
...
The anti-pattern. throw new UserLoadException(e.getMessage()) keeps the text only. The SQLException frames — the very ones that show which statement and which driver failed — are gone for good, and the incident is left with the line cannot load user 42 and no lead at all.
If the exception object already exists (built by a factory, say), the cause can be supplied once via initCause(e) — a second call throws IllegalStateException.
Do not confuse this with suppression: addSuppressed is for secondary failures while closing resources, whereas cause answers "what made this happen".