Borrow and BorrowMut Traits
Understand the Borrow and BorrowMut traits, their purpose beyond AsRef, and how they enable flexible key lookups in hash-based collections
The Borrow and BorrowMut traits live in std::borrow. They look very much like AsRef and AsMut — you call borrow() to get a &T, or borrow_mut() to get a &mut T — but they carry an extra guarantee that AsRef does not. That guarantee is what makes HashMap::get accept a &str when the key type is String.
If you already know AsRef, the easiest way to think about Borrow is this: AsRef says “I can give you a reference to T cheaply”. Borrow says the same thing, and additionally promises that the borrowed T is equivalent to the original value for hashing and equality. That single promise makes a whole class of ergonomic APIs possible, but it also means you cannot implement Borrow casually.
The Problem Borrow Solves
Imagine a HashMap<String, u32> that maps company names to employee counts. You want to look up a value with a string slice you already have:
let mut map = HashMap::new();
map.insert("AcmeCorp".to_string(), 150);
let query = "AcmeCorp"; // &str
let count = map.get(query); // ❌ won't compile
HashMap::get expects a reference to the key type, which is &String. A &str is not a &String, so the compiler rejects it — even though logically they represent the same text. You could write map.get(&query.to_string()), but that allocates a temporary String on every lookup, which defeats the purpose of using a fast hash map.
The standard library solves this by making get generic over any type Q such that the key type K implements Borrow<Q>. When K is String, String: Borrow<str>, so get can accept a &str. The lookup hashes and compares the &str as if it were the original String, and finds the entry without an allocation.
Borrow formalises the idea that a borrowed form is a true stand‑in for the owned value, not just a convenient pointer.
No allocation on lookup:
When you call map.get("some_key") on a HashMap<String, V>, the compiler resolves it through Borrow<str>. No String allocation occurs — the &str itself is hashed and compared directly.
What Borrow and BorrowMut Define
The traits are minimal:
pub trait Borrow<Borrowed: ?Sized> {
fn borrow(&self) -> &Borrowed;
}
pub trait BorrowMut<Borrowed: ?Sized>: Borrow<Borrowed> {
fn borrow_mut(&mut self) -> &mut Borrowed;
}
Both traits are generic over the target type you want to borrow as — the Borrowed parameter. String: Borrow<str> means “a String can be borrowed as a str”. Vec<T>: Borrow<[T]> means “a Vec<T> can be borrowed as a [T] slice”.
BorrowMut is a subtrait of Borrow. If a type implements BorrowMut<U>, it automatically provides an immutable borrow() as well. The mutable version is often used when you want to mutate through the borrowed form, for example peeking into a Vec<u8> through a &mut [u8].
The mechanics are exactly what you would expect — calling borrow() returns a reference without moving the original — but the real weight of the trait lies in the semantic contract.
The Critical Invariant: Hash and Eq Consistency
The Rust standard library documents Borrow with a specific contract: if T: Borrow<U>, then for any two values a and b of type T, the equality and hash of a.borrow() must be identical to that of a itself when compared via the same Eq and Hash implementations.
In other words, x.borrow() == y.borrow() must always agree with x == y, and hash(x.borrow()) must always agree with hash(x). This is not checked by the compiler; violating it leads to logic bugs that are extremely hard to track down.
Consider a HashMap<MyType, V> where MyType: Borrow<Id>. The map uses MyType’s Eq and Hash to place entries. When you call get with an &Id, the map hashes and compares that Id. If the Id derived from MyType through borrow() does not faithfully represent the same identity, lookups will miss existing entries, or worse, find the wrong one.
For the standard String: Borrow<str> implementation, the invariant holds trivially: a String’s content is the str, so hashing and equality are identical. The same is true for Vec<T>: Borrow<[T]>.
The compiler cannot check the invariant:
If you implement Borrow<U> for a custom type and return a &U that hashes or compares differently from the original value, your HashMap will silently produce wrong results. The type system trusts you to uphold the contract.
Standard Library Implementations
The standard library provides blanket implementations so that typical patterns work out of the box:
T: Borrow<T>for everyT. Every value can be borrowed as itself.&T: Borrow<T>for everyT. A reference to a value can be borrowed as the value.String: Borrow<str>— the canonical example for hash map lookups.Vec<T>: Borrow<[T]>— similar for slices.Box<T>: Borrow<T>for anyT: ?Sized.Cow<str>: Borrow<str>and analogous for[T], etc.
BorrowMut follows the same pattern: T: BorrowMut<T> when T is mutable, String: BorrowMut<str>, Vec<T>: BorrowMut<[T]>, and Box<T>: BorrowMut<T>.
These blanket implementations mean you rarely need to think about Borrow directly in day‑to‑day code — you just call HashMap::get with a &str and it works. Understanding the mechanism matters when you want the same flexibility for your own key types.
Using Borrow for Flexible Key Lookups in HashMap
The HashMap methods that take a key — get, contains_key, remove, and the Entry API — are all generic over Q where K: Borrow<Q>. The signature of get looks like this:
impl<K, V, S> HashMap<K, V, S> {
pub fn get<Q: ?Sized>(&self, k: &Q) -> Option<&V>
where
K: Borrow<Q>,
Q: Hash + Eq,
{
// ...
}
}
Notice Q does not have to be the same type as K. It only needs to be a type that K can faithfully borrow as, and that itself implements Hash and Eq. That is how "key" (a &str) can be used to query a HashMap<String, V> — String implements Borrow<str>, and str implements Hash and Eq.
A realistic example:
use std::collections::HashMap;
fn main() {
let mut conferences = HashMap::new();
conferences.insert("RustConf".to_string(), 2025);
conferences.insert("RustNation".to_string(), 2026);
let search = "RustConf";
if let Some(year) = conferences.get(search) {
println!("RustConf is in {}", year);
}
}
The get call passes &str, which matches Q = str. No allocation. The hash map computes the hash of the str "RustConf" and finds the entry. If we had used a HashMap<String, u32> and called get with &String, it would also work via String: Borrow<String>.
This flexibility extends to any type you design as a key, provided you implement Borrow correctly for the lookup type you want to support.
How Borrow Differs from AsRef and AsMut
AsRef and AsMut are generic conversion traits: they mean “you can get a cheap reference to T from this type”. They carry no semantic contract about equality or hashing. A type can implement AsRef<str> without the returned string slice having any meaningful relationship to the original value’s identity.
Because AsRef lacks the equivalence promise, the standard library cannot use it for hash map lookups. If HashMap::get were based on AsRef, a type could return an arbitrary &str from as_ref(), and the hash table would become inconsistent.
You can see the distinction concretely in std::path::PathBuf. It implements both AsRef<Path> and Borrow<Path>. AsRef<Path> is the general “get a path reference” method. Borrow<Path> additionally guarantees that the borrowed Path hashes and compares identically to the PathBuf — which is true because a PathBuf is just an owned Path.
When to use which trait:
If your function just needs to accept anything that can produce a &T, use AsRef<T> as a bound — you do not need the hashing guarantee. Reserve Borrow<T> for cases where the caller will use the borrowed value for hash‑based lookups or when equality must be preserved.
The mutable counterparts follow the same logic: AsMut is for cheap mutable reference conversion; BorrowMut is AsMut with the added equivalence promise. In practice, BorrowMut is used far less often than Borrow because the key‑lookup pattern is the primary motivation.
Implementing Borrow for Your Own Types
Say you have a custom type UserId that wraps a u64, and you want to use it as a key in a HashMap. You also want to be able to look up entries with a plain u64 without constructing a temporary UserId.
use std::borrow::Borrow;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
#[derive(Debug, PartialEq, Eq, Hash)]
struct UserId(u64);
impl Borrow<u64> for UserId {
fn borrow(&self) -> &u64 {
&self.0
}
}
fn main() {
let mut sessions = HashMap::new();
sessions.insert(UserId(42), "active");
sessions.insert(UserId(99), "idle");
// Look up by u64 directly
assert_eq!(sessions.get(&42u64), Some(&"active"));
}
The implementation is straightforward: borrow() returns a reference to the inner u64. The equivalence invariant holds because the UserId's equality and hash are entirely based on the inner u64. The compiler cannot verify that, but it’s true by construction.
A more subtle case arises when the borrowed form is not a direct field but a computed value. If you implement Borrow<str> for a type by extracting a substring, you must ensure that different original values that are equal under your Eq produce identical &str results, and that the hash of the &str matches the hash of the original. In most cases, you’ll avoid computing borrowed forms — just return a reference to an existing field.
Don't implement Borrow for unrelated types:
Resist the temptation to implement Borrow<U> just because you can return a &U. The contract ties U to the identity of the original value. If U captures only part of that identity, the invariant will be violated, and hash‑based collections will break silently.
Common Pitfalls and Misconceptions
Misconception: Borrow is just a more convenient AsRef
AsRef is for generic reference conversion — any function that needs “something that can give me a &T” should accept impl AsRef<T>. Borrow is specifically for hash‑based key lookups where the borrowed form is a true equivalent. Using Borrow where AsRef would suffice over‑constrains the caller and can mislead readers about the contract.
Pitfall: Violating the hash/eq invariant accidentally
If your type’s Eq and Hash derive from multiple fields, but borrow() only returns a reference to one of those fields, lookups will be inconsistent. For example, a Student { id: u64, name: String } implementing Borrow<u64> is only safe if Eq and Hash are also based solely on id. If Eq compares name as well, a HashMap using Student keys will put two students with the same id but different names into different buckets, but a lookup with a &u64 will only find one of them.
Trait object lifetime sensitivity
In older Rust editions, calling borrow_mut() on a Box<dyn Trait> could produce perplexing lifetime errors. The issue stemmed from how trait object lifetime defaults interacted with the borrow. Modern Rust (since NLL stabilisation) handles these cases correctly, but the underlying lesson remains: when you have a Box<dyn SomeTrait> and you call borrow_mut(), the returned mutable reference has a lifetime tied to the box — it cannot outlive the box itself. This is usually what you want, but in complex generic code with manual lifetime annotations, you might need to be explicit.
The following code, which failed in Rust 1.14, compiles without complaint today:
use std::borrow::BorrowMut;
trait T {}
impl T for u8 {}
fn main() {
let mut r: Box<dyn T> = Box::new(23u8);
let _: &mut dyn T = r.borrow_mut();
}
If you ever encounter a similar error in generic code involving trait objects, the first thing to check is whether you have accidentally asked for a 'static borrow by omitting lifetime parameters.
Summary and Next Steps
Borrow is not a cosmetic variation of AsRef. It is a promise that the borrowed form represents the same identity as the original value with respect to hashing and equality. That promise is what makes HashMap::get("key") work without allocations, and it is what you must uphold if you implement Borrow for your own key types.
The most important insight: every time you see Borrow<Q> in an API, it exists so that you can query a collection using Q as a key without owning a Q. The trait is the glue between owned keys and borrowed query values.
Together, Borrow and ToOwned form a round‑trip between borrowed and owned data that the standard library relies on heavily.
You now understand the key distinction:
If you can explain why HashMap::get uses Borrow instead of AsRef, you have grasped the trait’s purpose. Everything else is implementation detail.