blob: 360757e6b95a1aa80767082b2d490336a4c7b390 [file] [view] [edit]
Throwables should not override `equals()` or `hashCode()`.
1. **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
`IllegalArgumentException`s 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.
2. **The Stack Trace Problem** When an exception is instantiated (or thrown),
Java populates its stack trace via `fillInStackTrace()`.
* Complexity: Are you going to include the stack trace in your `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()`.
* Brittleness: If you don't include the stack trace, you are saying that
an exception thrown in `ServiceA` is equal to an exception thrown in
`ServiceB` just because they share a message or an error code. This
masks critical debugging context.
3. **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.
4. **Java Still Sometimes Compares Exceptions by Object Identity** Even if two
exceptions are "the same" according to `equals`, Java will still print both
if it encounters them during a call to `printStackTrace`, and it will allow
one to be a suppressed exception of the other. In both ways, two exceptions
that are "the same" will continue to be treated as different by Java.
### **Recommended Alternative: A separate value object**
If you find yourself needing to compare exceptions, **extract the state into a
separate value object.**
Instead of this:
```java
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.
```java
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())) { ... }
```