What is the Borrow trait vs AsRef

Borrow treats wrappers as references to inner data, while AsRef converts values to references of other types for generic flexibility.

When a reference isn't enough

You write a function to parse a configuration value. It takes &str. You call it with a String from std::env::var(). The compiler rejects you. You add .as_str() at the call site. It works. Then you see Borrow in the standard library documentation and notice HashMap uses it. You try to use Borrow in your own code to make things generic. The compiler rejects your callers with a trait bound error.

The confusion is real. Borrow and AsRef both turn values into references. They look identical in usage. They both let you pass a String where a &str is expected. Yet the standard library uses Borrow for collections and AsRef for almost everything else. Using the wrong one breaks generic code or forces unnecessary allocations.

AsRef is the general-purpose adapter for your APIs. Borrow is the specialized key for collections. One is about conversion. The other is about identity.

The adapter and the key

Think of AsRef as a universal power adapter. You have a device with a specific plug. The socket expects a different shape. AsRef says, "I can adapt my output to fit this socket." It's flexible. It allows you to write a function that accepts a String, a &str, a Box<str>, or any custom type that knows how to produce a &str. The function doesn't care what the input is. It only cares that it can get a reference to the data it needs.

Borrow is different. It's like a key that opens a lock. The lock expects a specific cut. Borrow says, "I contain this exact shape inside me, and I can hand you a reference to it without changing what I am." Borrow is rarely used directly in user code. It lives inside std::collections. It allows HashMap to look up a String key using a &str without allocating a new String. The collection stores owned keys. You query with a borrowed reference. Borrow bridges that gap safely.

The distinction matters because Borrow carries a contract that AsRef does not. AsRef is a conversion. Borrow is an access pattern with guarantees about hashing and equality.

Minimal example

Both traits let you extract a reference. The syntax is nearly identical. The difference lies in where you apply them.

use std::borrow::Borrow;
use std::convert::AsRef;

/// Accepts anything that can convert to a &str.
/// This is the standard pattern for generic string functions.
fn print_as_ref<T: AsRef<str>>(input: T) {
    // Convert the input to &str.
    // The compiler generates the call to .as_ref().
    let text: &str = input.as_ref();
    println!("AsRef: {}", text);
}

/// Accepts anything that can borrow a &str.
/// This pattern is rare outside of collection implementations.
fn print_borrow<T: Borrow<str>>(input: T) {
    // Get a reference to the inner str.
    // The compiler generates the call to .borrow().
    let text: &str = input.borrow();
    println!("Borrow: {}", text);
}

fn main() {
    let owned = String::from("hello");
    let borrowed: &str = "world";

    // AsRef works with owned and borrowed data.
    print_as_ref(owned);
    print_as_ref(borrowed);

    // Borrow also works with owned and borrowed data.
    print_borrow(owned);
    print_borrow(borrowed);
}

The output is identical. Both functions accept String and &str. The standard library implements AsRef<str> for String, &str, Box<str>, and Cow<str>. It also implements Borrow<str> for the same types. The overlap is intentional. The divergence happens when you write your own types or when you need the guarantees that Borrow provides.

The hidden contract of Borrow

Here is the technical reason Borrow exists. It's not just about getting a reference. It's about hashing and equality.

When HashMap looks up a key, it hashes the query value and compares it against the stored keys. If the query is a &str and the key is a String, the hash of the &str must match the hash of the String. The equality check must also match. Borrow enforces this.

If K: Borrow<Q>, the standard library guarantees that Q hashes the same as K and compares equal the same way. This allows HashMap<K, V> to implement get with a bound on Borrow<K>. You can pass a &str to look up a String key, and the lookup works correctly.

AsRef has no such guarantee. AsRef is a conversion. A type could implement AsRef<u32> and return a random number every time. Or it could implement AsRef<str> and return a trimmed version of the string. AsRef is about transformation. Borrow is about identity.

This is why Borrow is tied to Hash and Eq in the collection APIs. The trait bound Q: ?Sized + Hash + Eq + Borrow<K> ensures that the query value is compatible with the key for hashing and comparison. AsRef cannot provide this safety. If HashMap used AsRef, you could pass a type that converts to the key type but hashes differently, breaking the map's invariants.

Treat Borrow as the mechanism that keeps collections fast and correct. AsRef is for your code where you just need to read data.

Realistic example: File paths

File operations are the most common use case for AsRef. Paths can come from string literals, String values, Path references, or PathBuf owned buffers. Writing a function that accepts only &str forces callers to convert. Writing a function that accepts AsRef<Path> makes the API flexible and idiomatic.

use std::fs;
use std::path::Path;

/// Reads a file and returns its contents.
/// Accepts any type that can convert to a &Path.
/// This covers &str, String, &Path, and PathBuf.
fn read_config<P: AsRef<Path>>(path: P) -> Result<String, std::io::Error> {
    // Convert the generic input to &Path once.
    // This avoids repeated conversions inside the function.
    let p: &Path = path.as_ref();
    
    // Use the path for filesystem operations.
    fs::read_to_string(p)
}

fn main() {
    // Call with a string literal.
    // The compiler infers P as &str, which implements AsRef<Path>.
    match read_config("config.toml") {
        Ok(content) => println!("Config: {}", content),
        Err(e) => eprintln!("Error: {}", e),
    }

    // Call with a PathBuf constructed from an iterator.
    // The compiler infers P as PathBuf, which implements AsRef<Path>.
    let dir = std::env::current_dir().unwrap();
    let file = dir.join("config.toml");
    match read_config(file) {
        Ok(content) => println!("Config from PathBuf: {}", content),
        Err(e) => eprintln!("Error: {}", e),
    }
}

Community convention dictates that file operations accept AsRef<Path>. You'll see this pattern in std::fs, std::env, and almost every crate that touches the filesystem. Following this convention makes your code feel native to Rust developers. Callers can pass whatever they have without boilerplate.

Don't write fn read(s: &str) and force callers to do .as_str() or .to_str().unwrap(). Use AsRef<Path>. It handles the conversion safely and efficiently.

Pitfalls and compiler errors

Using Borrow in public function signatures is a common mistake. Borrow is a tighter contract than AsRef. Many types implement AsRef but not Borrow. If you constrain your function with Borrow, you reject valid inputs.

use std::borrow::Borrow;
use std::convert::AsRef;

/// A custom type that wraps a string.
struct CustomStr(String);

/// Implement AsRef<str> for CustomStr.
/// This allows CustomStr to be used where &str is expected.
impl AsRef<str> for CustomStr {
    fn as_ref(&self) -> &str {
        &self.0
    }
}

/// This function requires Borrow<str>.
/// CustomStr does not implement Borrow<str>.
fn requires_borrow<T: Borrow<str>>(input: T) {
    let _s: &str = input.borrow();
}

fn main() {
    let custom = CustomStr(String::from("test"));
    
    // This call fails to compile.
    // The compiler rejects this with E0277 (the trait bound `CustomStr: Borrow<str>` is not satisfied).
    // requires_borrow(custom);
}

The error E0277 tells you that CustomStr doesn't implement Borrow<str>. It only implements AsRef<str>. If you change the bound to AsRef<str>, the code compiles.

Another pitfall is assuming Borrow and AsRef are interchangeable in generic bounds. They are not. Borrow implies AsRef in the standard library, but the reverse is not true. Code that works with Borrow will always work with AsRef for standard types, but custom types often only implement AsRef.

Prefer AsRef for public APIs. Borrow is for collections and internal library code. If you reach for Borrow in your own function signature, stop and ask if AsRef does the job. In 99% of cases, it does.

Don't use Borrow to solve a problem AsRef handles. You'll alienate callers who only implement the standard conversion traits.

Decision: Borrow vs AsRef

Use AsRef<T> when you're writing a function that accepts multiple input types and only needs to read the data. Use AsRef<T> for file paths, strings, and slices where the caller might pass owned or borrowed data. Use AsRef<T> when you want your API to be flexible and idiomatic. Use Borrow<T> when you're implementing a collection that needs to look up keys using references without allocating. Use Borrow<T> inside std::collections implementations to bridge owned keys and borrowed query values. Reach for &T directly when you only want to accept references and don't care about owned types. Avoid Borrow in public function signatures unless you have a specific reason to match collection behavior.

Stick to AsRef for your APIs. Let the standard library worry about Borrow.

Where to go next