Exception In GetHashCode

ID

csharp.exception_in_gethashcode

Severity

high

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Reliability

Language

CSharp

Tags

equals, reliability

Description

Reports overrides of GetHashCode() that contain a throw statement. GetHashCode is invoked implicitly by every hash-based collection and LINQ operator; the runtime contract requires it to be total — it must never throw, even when the object is in an unexpected state.

Rationale

Dictionary, HashSet, Hashtable, ConcurrentDictionary, and LINQ operators such as GroupBy and Distinct all call GetHashCode while computing buckets and lookups. A throw at that point surfaces far from its cause, breaks the Equals/GetHashCode contract (equal objects must have equal hashes), and can leave hash-based collections in an inconsistent state where elements seem to disappear.

GetHashCode should treat all reachable object states as valid for hashing — combining the available immutable fields and returning 0 when no fields are available is far better than throwing.

public class Item
{
    private readonly string id;

    public override int GetHashCode()
    {
        if (id == null)
            throw new InvalidOperationException("id missing"); // FLAW
        return id.GetHashCode();
    }

    public override bool Equals(object obj) => obj is Item other && id == other.id;
}

public class ItemOk
{
    private readonly string id;

    public override int GetHashCode()                          // OK, never throws
    {
        return id?.GetHashCode() ?? 0;
    }

    public override bool Equals(object obj) => obj is ItemOk other && id == other.id;
}

Remediation

Treat null and other unexpected states as valid input for hashing. Use the null-conditional operator (field?.GetHashCode() ?? 0), HashCode.Combine, or any equivalent pattern that returns a stable integer for every reachable object state.