Exception In Equals

ID

csharp.exception_in_equals

Severity

high

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Reliability

Language

CSharp

Tags

equals, reliability

Description

Reports overrides of Equals(object) that contain a throw statement. Equals must never throw: the runtime contract requires it to accept any argument, including null, and return either true or false.

Rationale

Equals is invoked implicitly by many framework APIs: hash tables and dictionaries during lookup, LINQ operators such as Contains and Distinct, the == operator on user-defined types, equality comparers, and serialisation. Throwing from Equals surfaces failures in unrelated, hard-to-diagnose call sites, breaks the runtime contract, and can corrupt collection invariants.

The supported response for an argument that cannot be compared is to return false, not to throw.

public class Money
{
    private readonly decimal amount;

    public override bool Equals(object obj)
    {
        if (obj is not Money)
            throw new ArgumentException("not a Money"); // FLAW — Equals must not throw
        return amount == ((Money)obj).amount;
    }
}

public class MoneyOk
{
    private readonly decimal amount;

    public override bool Equals(object obj)            // OK — returns false for foreign types
    {
        return obj is MoneyOk other && amount == other.amount;
    }

    public override int GetHashCode() => amount.GetHashCode();
}

Remediation

Make Equals return false when the argument is null or of an unrelated type. Use pattern matching (obj is MyType other) to combine the type check and the cast safely, and never call a member of obj without confirming its type first.