Custom Hash and Eq
To use a type as a HashMap key or HashSet member, it must implement Hash and Eq. Derive Macros and Common Traits showed that #[derive(Hash, Eq, PartialEq)] handles this for most types. But the derive isn't always what you want, sometimes only part of a struct should determine identity (an ID field, not a cached timestamp), and that's where hand-implementing comes in. The catch is a contract between the two traits that's easy to violate and produces bugs that are genuinely nasty to debug: keys that vanish from a map they're supposedly in. This tutorial covers that contract, when to hand-write, and how to do it without breaking things.
The Contract: Equal Values Must Hash Equal
There is one inviolable rule linking Hash and Eq: if two values are equal (a == b), they must produce the same hash. A HashMap uses the hash to find the bucket, then Eq to confirm the match; if equal keys hash differently, a lookup goes to the wrong bucket and the entry appears to not exist.
use std::collections::HashMap;
// the derived impls satisfy the contract automatically — hash and eq use the SAME fields
#[derive(Hash, PartialEq, Eq)]
struct UserId(u64);
let mut map = HashMap::new();
map.insert(UserId(42), "Alice");
assert_eq!(map.get(&UserId(42)), Some(&"Alice")); // found — hash and eq agree
When you #[derive] both, they're computed from the same set of fields, so the contract holds by construction. The danger appears only when you hand-write one of them, or derive one and hand-write the other, and accidentally make them consider different fields. The reverse direction (different values may share a hash) is fine and expected, that's a hash collision, which Eq resolves. Only "equal but different hashes" is a bug.
Hashing a Subset of Fields
The main legitimate reason to hand-implement is when a struct's identity depends on only some of its fields. A cached or derived field, a timestamp, a memoized value, shouldn't affect whether two records are "the same":
use std::hash::{Hash, Hasher};
struct User {
id: u64, // identity
name: String, // identity
last_seen: u64, // NOT identity — a mutable cache, must be ignored
}
impl PartialEq for User {
fn eq(&self, other: &Self) -> bool {
self.id == other.id && self.name == other.name // ignores last_seen
}
}
impl Eq for User {}
impl Hash for User {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
self.name.hash(state); // hashes the SAME fields eq uses
}
}
The critical discipline: Hash and Eq must consider the exact same fields. Here both use id and name and both ignore last_seen, so the contract holds, two Users with the same id/name are equal and hash equal regardless of last_seen. Get this out of sync (hash last_seen but don't compare it, or vice versa) and you reintroduce the "equal but different hash" bug.
Gotcha: the failure mode of a broken
Hash/Eqcontract is uniquely nasty. Youinserta key, thengetit back with an equal key, and getNone, the entry is in the map but unreachable, because the lookup hashes to a different bucket than the insert did. It doesn't panic or error; the map just silently behaves as if the key isn't there, and iterating the map does show it. Any time aHashMap"loses" a key you're certain you inserted, suspect a hand-writtenHashandEqthat disagree on which fields matter.
The Derive Trap: Deriving One, Hand-Writing the Other
A subtle version of the same bug: you hand-write PartialEq to ignore a field, but leave #[derive(Hash)] in place (or vice versa). Now they silently disagree.
// BUG: Hash is derived (hashes ALL fields) but Eq ignores last_seen
#[derive(Hash)] // hashes id, name, AND last_seen
struct User {
id: u64,
name: String,
last_seen: u64,
}
impl PartialEq for User {
fn eq(&self, o: &Self) -> bool { self.id == o.id && self.name == o.name } // ignores last_seen
}
impl Eq for User {}
// two Users equal by eq (same id/name, different last_seen) now hash DIFFERENTLY → broken
The fix is the rule: if you hand-write one of Hash/Eq, hand-write the other to match. Don't mix a derived one with a custom one unless you've verified they consider identical fields. When in doubt, the safest design is to not store non-identity fields in the key type at all, put them in the value, so the key can be cleanly derived.
Newtype Keys: A Cleaner Alternative
Often the better move is to sidestep custom impls entirely by making the key a dedicated newtype (Newtype Pattern) containing only the identity fields, and keeping everything else in the value:
use std::collections::HashMap;
#[derive(Hash, PartialEq, Eq)] // fully derived — contract holds trivially
struct UserKey { id: u64, name: String }
struct UserData { last_seen: u64, email: String } // non-identity fields
let mut users: HashMap<UserKey, UserData> = HashMap::new();
Now UserKey derives all three traits from all its fields (they're all identity), so the contract is automatic, and the mutable/cached data lives in UserData where it can change freely without affecting lookup. This is usually cleaner than a hand-written partial Hash/Eq: the type system expresses "these fields are the identity" directly, and there's no contract to accidentally break. Reach for a custom impl only when you can't restructure the key this way.
Deciding How to Implement
| Situation | Do this |
|---|---|
| All fields define identity | #[derive(Hash, PartialEq, Eq)] |
| Only some fields define identity | newtype key of just those fields (preferred), or hand-write both |
Hand-writing Hash or Eq | hand-write the other; use the same fields in each |
| Non-identity/cached data | keep it in the value, not the key |
Key needs &str lookup on a String field | derive normally; Borrow handles it (Deref, AsRef, Borrow) |
The overriding principle: Hash and Eq must agree on exactly which fields constitute identity. Deriving both guarantees it for free; the moment you hand-write either, you take on the obligation to keep them in sync, and the penalty for failing is a silent, hard-to-trace "lost key" bug. Prefer restructuring (a newtype key) over a custom impl whenever you can.
Key Takeaways
- The
Hash/Eqcontract: equal values must hash equal. AHashMapfinds the bucket by hash then confirms withEq, so if equal keys hash differently, lookups silently fail. #[derive(Hash, PartialEq, Eq)]satisfies the contract automatically because all three use the same fields. Collisions (different values, same hash) are fine,Eqresolves them; only "equal but different hash" is a bug.- Hand-implement when only some fields define identity (ignore caches/timestamps), but
HashandEqmust consider the exact same fields, or you get the "key is in the map but unreachable" bug. - Never mix a derived
Hashwith a hand-writtenEq(or vice versa) unless they consider identical fields, the derive hashes all fields and will disagree with a partialEq. - Prefer a newtype key containing only identity fields (with everything else in the value) over a custom impl, it makes the contract hold by construction and removes the chance to break it.