Throwables should not override equals() or hashCode().
Exceptions are Events, Not Value Objects Philosophically, an exception
represents a unique historical event: something went wrong at a specific
time, in a specific thread, at a specific line of code. It is not a data
container or a value type (like a String or a Money object). Even if two
IllegalArgumentExceptions are thrown with the exact same message ("ID
cannot be null"), and perhaps even identical stack traces, they represent
two distinct failures that happened independently. Treating them as “equal”
conceptually conflates two different events.
The Stack Trace Problem When an exception is instantiated (or thrown),
Java populates its stack trace via fillInStackTrace().
equals()
comparison? If you do, comparing arrays of StackTraceElement is
computationally expensive. Exceptions can also have causes and
suppressed exceptions. This adds expense, too. Also, will all the
transitive causes and suppressed exceptions themselves implement
equals() the way you want? Plus, causes and suppressed exceptions make
exceptions mutable, and mutable objects generally shouldn’t implement
equals().ServiceA is equal to an exception thrown in
ServiceB just because they share a message or an error code. This
masks critical debugging context.It Hides Bad Architecture The primary reason you would need to override
these methods is if you are placing Exceptions into a HashSet, or using
them as keys in a HashMap. If you are doing this, you are likely using
Exceptions for normal business logic or control flow, which is a known
anti-pattern. Exceptions are for exceptional circumstances; they are heavy
(because of the stack trace) and slow to generate.
If you find yourself needing to compare exceptions, extract the state into a separate value object.
Instead of this:
public class UserNotFoundException extends RuntimeException {
private final String userId;
private final String groupId;
// Override equals and hashCode based on userId and groupId...
}
Do this: Create a custom Exception that contains a value object (like a Record or a POJO), and compare the value objects instead.
public class UserNotFoundException extends RuntimeException {
private final ErrorDetails details;
public UserNotFoundException(ErrorDetails details) {
super("%s is not a member of %s:".formatted(details.userId(), details.groupId()));
this.details = details;
}
public ErrorDetails getDetails() { return details; }
}
// Compare the details, not the exceptions!
if (e1.getDetails().equals(e2.getDetails())) { ... }
Suppress false positives by adding the suppression annotation @SuppressWarnings("ThrowableEqualsHashCode") to the enclosing element.