Why these traits matter

In real Rust code, you often want a function to accept both owned and borrowed values:

  • String and &str
  • PathBuf and &Path
  • Vec<u8> and &[u8]

You could write separate overloads, but Rust does not support function overloading in the traditional sense. Instead, you use traits to accept a broader set of inputs.

The key best practice is this:

  • Use AsRef<T> when you need a cheap view into a value as another type.
  • Use Borrow<T> when the borrowed form must behave like the original type for equality and hashing, especially in collections.

That distinction is small but important.


AsRef: lightweight conversion for API flexibility

AsRef<T> means “I can cheaply provide a reference to T.” It is ideal for functions that only need read-only access to a value in a particular form.

Typical use cases

  • Accepting &str, String, or Cow<str> in one function
  • Accepting &Path and PathBuf
  • Working with byte slices without forcing allocation

Example: accepting path-like inputs

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

fn read_config<P: AsRef<Path>>(path: P) -> io::Result<String> {
    fs::read_to_string(path.as_ref())
}

This function can be called with:

let a = read_config("config.toml");
let b = read_config(std::path::PathBuf::from("config.toml"));
let c = read_config(Path::new("config.toml"));

The function only needs a &Path, so AsRef<Path> is a natural fit.

Best practices for AsRef

  1. Use it at API boundaries.
  2. It improves ergonomics for callers without forcing conversions.

  1. Keep the conversion shallow.
  2. AsRef should not imply ownership transfer or expensive computation.

  1. Prefer the most specific target type.
  2. If your function needs a &Path, use AsRef<Path>, not a broader abstraction.

  1. Avoid using it for semantic equivalence.
  2. AsRef does not guarantee that two values compare the same way after conversion.


Borrow: for semantic equivalence and collection lookups

Borrow<T> is stronger than AsRef<T>. It means the borrowed form should act like the original type for equality, ordering, and hashing. This matters most when you use a borrowed key to look up an owned key in a map or set.

Example: HashMap<String, V> lookup with &str

Rust’s standard collections use Borrow to support borrowed lookups:

use std::collections::HashMap;

let mut users = HashMap::new();
users.insert(String::from("alice"), 42);

assert_eq!(users.get("alice"), Some(&42));

Here, HashMap<String, V> can be queried with &str because String: Borrow<str>. The borrowed key type str is semantically equivalent to the owned key String for hashing and equality.

Why this matters

If you implement your own key type and want borrowed lookups to work, Borrow is the trait that enables it. This is especially useful for:

  • caches
  • indexes
  • registries
  • in-memory maps keyed by owned strings or paths

Best practices for Borrow

  1. Use it when borrowed and owned forms are logically the same key.
  2. For example, String and str.

  1. Do not use it for “similar enough” types.
  2. If equality or hashing changes after borrowing, Borrow is the wrong trait.

  1. Make sure Eq, Ord, and Hash are consistent.
  2. The borrowed and owned forms must behave identically for collection semantics.

  1. Use it to support efficient lookups.
  2. It avoids allocating temporary owned values just to search a map.


AsRef vs Borrow: choosing the right trait

A simple rule of thumb helps:

TraitPrimary purposeCommon useKey requirement
AsRef<T>Cheap reference conversionAPI flexibilityNo semantic guarantees
Borrow<T>Borrowed lookup and equivalenceCollections and key typesSame Eq/Hash/Ord behavior

Practical decision guide

Use AsRef if:

  • you only need a borrowed view
  • you are reading data
  • you do not care about lookup semantics
  • you want ergonomic function parameters

Use Borrow if:

  • you are implementing or using map/set key behavior
  • you need borrowed lookups without allocation
  • the borrowed form must be interchangeable with the owned form

A real-world example: a file cache

Suppose you are building a cache keyed by file paths. You want callers to query it with &Path, PathBuf, or &str.

Good design with Borrow

use std::collections::HashMap;
use std::path::{Path, PathBuf};

struct FileCache {
    entries: HashMap<PathBuf, String>,
}

impl FileCache {
    fn new() -> Self {
        Self {
            entries: HashMap::new(),
        }
    }

    fn insert<P: Into<PathBuf>>(&mut self, path: P, contents: String) {
        self.entries.insert(path.into(), contents);
    }

    fn get<P: AsRef<Path>>(&self, path: P) -> Option<&String> {
        self.entries.get(path.as_ref())
    }
}

This works well because:

  • insert takes ownership when needed via Into<PathBuf>
  • get accepts any path-like input via AsRef<Path>
  • the internal map uses PathBuf, and lookups can use borrowed &Path

Why not use Borrow in get directly?

For a simple wrapper API, AsRef<Path> is usually enough. The collection itself already uses Borrow internally for borrowed lookup support. Your wrapper only needs to convert the input to a &Path.

If you were designing a custom key type or a custom lookup abstraction, then Borrow would become more relevant.


Avoid overgeneralizing conversion traits

A common mistake is to make every function generic over every conversion trait “just in case.” That can reduce clarity and sometimes create surprising trait bounds.

Overly generic example

fn log_message<T: AsRef<str>>(msg: T) {
    println!("{}", msg.as_ref());
}

This is fine for a simple logger. But if the function later needs to store the message, format it, or use it asynchronously, the generic signature may become awkward. In those cases, accepting impl Into<String> or &str may be more appropriate.

Better approach

Choose the narrowest trait that matches the function’s actual needs:

  • &str if the function only reads a string slice
  • AsRef<str> if you want ergonomic borrowed input
  • Into<String> if the function needs ownership
  • Borrow<str> only when key semantics matter

The best API is not the most generic one; it is the one that communicates intent clearly.


Common pitfalls and how to avoid them

1. Using Borrow when AsRef is enough

If you are not dealing with map keys or semantic equivalence, Borrow is usually unnecessary. It adds conceptual weight without improving the API.

Prefer:

fn parse_name<S: AsRef<str>>(input: S) -> &str {
    input.as_ref()
}

Not:

fn parse_name<S: Borrow<str>>(input: S) -> &str {
    input.borrow()
}

The second version suggests stronger semantics than the function needs.

2. Using AsRef for collection key lookups

If you are implementing a custom map or set and want borrowed key lookup, AsRef is not enough. It does not guarantee that the borrowed form hashes or compares the same way as the owned form.

3. Assuming AsRef is transitive

AsRef does not automatically chain through multiple layers in all contexts. If you need a specific borrowed type, require it explicitly.

4. Returning references from temporary conversions

A function like this is invalid:

fn bad<S: AsRef<str>>(s: S) -> &str {
    s.as_ref()
}

The returned reference would outlive the input parameter. The compiler will reject this, but it is a common design misconception. If you need to return owned data, return String or another owned type.


Designing ergonomic APIs without ambiguity

When writing reusable library code, follow these guidelines:

For input parameters

  • Use AsRef<T> for read-only borrowed access.
  • Use Into<T> when the function may need ownership.
  • Use concrete references like &T when the function is tightly scoped and genericity adds no value.

For key types in maps and sets

  • Ensure owned and borrowed forms are compatible.
  • Implement or rely on Borrow when borrowed lookup should work.
  • Keep Hash, Eq, and Ord consistent between forms.

For custom types

If you define a wrapper around a string or path, think carefully before implementing Borrow. Ask:

  • Is the borrowed form truly equivalent?
  • Will it preserve equality and hashing?
  • Do I want users to search collections with borrowed values?

If the answer is no, AsRef may still be appropriate for read-only access, but Borrow should be avoided.


Summary of recommended usage

ScenarioRecommended trait
Read-only function parameterAsRef<T>
Need ownershipInto<T>
Map/set borrowed lookupBorrow<T>
Exact type required&T or concrete type
Semantic equivalence requiredBorrow<T>

The practical difference is not just syntax. It affects how your API communicates intent and how safely it can be used in generic code.


A final checklist

Before choosing between AsRef and Borrow, ask:

  1. Do I only need a borrowed view? Use AsRef.
  2. Do I need borrowed lookup in a collection? Use Borrow.
  3. Will the borrowed form behave identically for hashing and equality? If not, do not use Borrow.
  4. Would a concrete reference be clearer? If yes, prefer it.
  5. Am I making the API generic for real benefit, or just for symmetry? Keep it simple.

Used well, these traits make Rust APIs more flexible without sacrificing correctness. Used carelessly, they can blur important semantic boundaries. The best practice is to choose the trait that matches the behavior you actually need, not the one that merely compiles.

Learn more with useful resources