Exception Raised Unexpected
ID |
kotlin.exception_raised_unexpected |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
Kotlin |
Tags |
best-practice, reliability |
Description
Reports override fun toString(), override fun hashCode() and
override fun equals(other: Any?) implementations whose body raises an
exception. These three methods are inherited from Any and are invoked by an
unpredictable set of callers — debuggers, IDE tooltips, logging frameworks,
hash-based collections — none of which expect them to fail.
Rationale
A toString() that throws makes a value invisible in the debugger: hovering
over it crashes the inspector. A hashCode() that throws prevents the object
from ever being placed into a HashMap or HashSet. An equals() that
throws breaks contains, removeAll, and almost every list/set algorithm in
the standard library.
class Bad(val id: Int) {
override fun toString(): String {
throw IllegalStateException("not initialised") // FLAW
}
override fun hashCode(): Int = throw RuntimeException("boom") // FLAW
}
The methods are also called from places that have no clean way to recover — a logging call cannot reasonably catch and swallow the exception, so the failure surfaces as a noisy stack trace in a log line and the original work is lost.
Remediation
Implement these methods so they always return a value. If a field can
legitimately be uninitialised, return a placeholder such as "<uninitialised>"
from toString() and 0 from hashCode(). Reserve exceptions for methods
the contract allows to throw.
class Good(val id: Int) {
override fun toString(): String = "Good(id=$id)"
override fun hashCode(): Int = id
override fun equals(other: Any?): Boolean = other is Good && other.id == id
}