Serguey Asael Shinder: The exception that closed your resource hides the one that broke your code
Here is a bug that costs an afternoon every time, and the stack trace in the log is not wrong — it is just not the interesting half.
You write the modern, correct thing:
try (Connection c = pool.get();
PreparedStatement ps = c.prepareStatement(SQL)) {
ps.setLong(1, id);
return ps.executeQuery();
}
The query fails. The resources are closed on the way out, and closing them fails too — the socket is already gone. Two exceptions, one try. Java has to pick which one propagates, and the language picks the one from the body: the close() failure is suppressed and attached to it.
That is Throwable.getSuppressed(), which the Javadoc defines as
"an array containing all of the exceptions that were suppressed, typically by the try-with-resources statement, in order to deliver this exception"
and it has been there since 1.7: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Throwable.html
Where it goes missing
printStackTrace() prints suppressed exceptions. It shows them indented under Suppressed:, with their own causes.
A great deal of production logging does not call printStackTrace(). It calls something closer to this:
catch (SQLException e) {
log.error("query failed: {}", e.getMessage());
}
getMessage() has no suppressed exceptions in it. No cause either. What reaches the log is one sentence from the outer failure, and the record of what happened while unwinding is discarded at the point where it was most useful.
The same hole opens in any code that rewraps:
catch (SQLException e) {
throw new DataAccessException("loading order " + id, e); // cause kept
}
The cause survives, because the constructor takes it. The suppressed array does not travel — it belongs to the object you just stopped propagating.

What to do instead
Pass the throwable, do not stringify it. Every logging façade has the overload:
catch (SQLException e) {
log.error("loading order {}", id, e); // the Throwable is the last argument
}
And when you rewrap, carry the suppressed ones across deliberately:
catch (SQLException e) {
DataAccessException wrapped = new DataAccessException("loading order " + id, e);
for (Throwable s : e.getSuppressed()) {
wrapped.addSuppressed(s);
}
throw wrapped;
}
Three lines, and the next person to read the log sees that the connection was already dead before the query ran — which is a different investigation from "the query is slow".
The rule underneath
An exception is a tree, and most logging flattens it to one node. Cause, causes of causes, suppressed — the interesting part is usually not the node at the top, because the node at the top is the one that ran last.
When a failure makes no sense, the first question is not "what threw this" but "what did this exception arrive carrying, and who dropped it". In my experience the answer is a getMessage() call somebody wrote to make the log tidy.