
Rust `Cow`: Designing APIs That Borrow When Possible and Clone When Necessary
What Cow is and why it matters
Cow<'a, B> is an enum in std::borrow with two variants:
Borrowed(&'a B)Owned(B::Owned)
The most common form is Cow<'a, str>, but it also works with slices and other borrowed/owned pairs.
The key idea is simple: if you only need read access, you can keep borrowing. If you need to modify the value, Cow can allocate only when mutation becomes necessary.
Typical use cases
Use Cow when:
- You want an API that accepts both
&strandString - Most inputs can be returned unchanged
- Only some inputs need normalization, escaping, filtering, or decoding
- You want to avoid unnecessary cloning in hot paths
Avoid Cow when:
- The data is always owned anyway
- You need frequent mutation regardless of input form
- The API would be clearer with separate borrowed and owned functions
The core mental model
Think of Cow as a “lazy ownership upgrade.”
You start with a borrowed value when possible. If you need to transform it, Cow can convert to owned data through to_mut() or by constructing an owned variant directly.
A small but important detail: Cow is not magic. It does not eliminate allocations in every case. It simply makes allocation conditional.
Example: normalize a string only if needed
use std::borrow::Cow;
fn normalize_username(input: &str) -> Cow<'_, str> {
if input.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_') {
Cow::Borrowed(input)
} else {
let normalized = input
.chars()
.filter_map(|c| {
let c = c.to_ascii_lowercase();
if c.is_ascii_alphanumeric() || c == '_' {
Some(c)
} else {
None
}
})
.collect::<String>();
Cow::Owned(normalized)
}
}
fn main() {
let a = normalize_username("alice_42");
let b = normalize_username("Alice-42!");
assert_eq!(a, "alice_42");
assert_eq!(b, "alice42");
}In this example, already-valid usernames are returned as borrowed data, while invalid ones are normalized into a new String.
When Cow improves API design
A common API design problem is accepting flexible input without forcing callers to allocate.
Consider a function that stores a label, logs a message, or performs validation. If the function takes String, callers with &str must allocate. If it takes &str, callers with owned strings may need to borrow temporarily, and you cannot return owned data easily.
Cow gives you a middle ground.
Accepting flexible input
use std::borrow::Cow;
fn render_tag(label: Cow<'_, str>) -> String {
format!("<span class=\"tag\">{label}</span>")
}
fn main() {
let owned = String::from("release");
let borrowed = "beta";
let a = render_tag(Cow::Borrowed(borrowed));
let b = render_tag(Cow::Owned(owned));
println!("{a}");
println!("{b}");
}This works, but it is a bit verbose for callers. In practice, you usually combine Cow with Into<Cow<'a, str>> to make the API ergonomic.
use std::borrow::Cow;
fn render_tag<'a, S>(label: S) -> String
where
S: Into<Cow<'a, str>>,
{
let label: Cow<'a, str> = label.into();
format!("<span class=\"tag\">{label}</span>")
}
fn main() {
let owned = String::from("release");
let borrowed = "beta";
let a = render_tag(borrowed);
let b = render_tag(owned);
println!("{a}");
println!("{b}");
}This pattern is common in library code because it lets the compiler infer the most efficient representation.
Cow in practice: mutation triggers allocation
The most useful method on Cow is to_mut(). It gives you a mutable reference to the owned data, cloning the borrowed data first if necessary.
Example: append a suffix only when needed
use std::borrow::Cow;
fn ensure_trailing_slash(path: Cow<'_, str>) -> Cow<'_, str> {
if path.ends_with('/') {
path
} else {
let mut path = path;
path.to_mut().push('/');
path
}
}
fn main() {
let a = ensure_trailing_slash(Cow::Borrowed("/api"));
let b = ensure_trailing_slash(Cow::Owned(String::from("/static/")));
assert_eq!(a, "/api/");
assert_eq!(b, "/static/");
}Notice the behavior:
- If the input already ends with
/, no allocation happens. - If it does not,
to_mut()ensures the value becomes owned before mutation.
This is a good fit for path normalization, query parameter rewriting, and text cleanup.
Choosing the right Cow target type
Cow is generic over a borrowed type B that implements ToOwned. The most common targets are:
| Type | Borrowed form | Owned form | Typical use |
|---|---|---|---|
Cow<'a, str> | &'a str | String | Text processing, labels, paths |
Cow<'a, [T]> | &'a [T] | Vec<T> | Slices, buffers, collections |
Cow<'a, Path> | &'a Path | PathBuf | Filesystem paths |
Cow<'a, OsStr> | &'a OsStr | OsString | Platform-native strings |
For application code, Cow<'a, str> is by far the most common. For systems and tooling code, Cow<'a, Path> is also very practical.
Example: path handling
use std::borrow::Cow;
use std::path::{Path, PathBuf};
fn normalize_config_path(path: Cow<'_, Path>) -> Cow<'_, Path> {
if path.is_absolute() {
path
} else {
let mut buf = PathBuf::from("/etc/myapp");
buf.push(path.as_ref());
Cow::Owned(buf)
}
}This function keeps absolute paths borrowed, but allocates a new PathBuf for relative paths.
Best practices for API authors
Cow is powerful, but it should be used deliberately. The goal is not to replace every &str or String parameter. The goal is to reduce friction and avoid unnecessary copies where it matters.
Prefer Into<Cow<'a, str>> for flexible inputs
If your function consumes the value or may need to normalize it, accepting Into<Cow<'a, str>> is often the most ergonomic option.
This allows callers to pass:
&'a strStringCow<'a, str>
Return Cow only when the caller benefits
Returning Cow is useful when the result may be borrowed from input or newly allocated. This is common in:
- sanitizers
- parsers
- canonicalizers
- lookup functions that may return a direct slice from an existing buffer
If the function always allocates, returning String is clearer.
Keep lifetimes simple
Cow does not remove lifetime complexity; it only packages it more conveniently. If your API has multiple borrowed inputs, make sure the lifetime relationships are obvious.
Avoid overusing Cow in internal code
Inside a function, converting everything to Cow can add noise. If the data is already owned and will stay owned, use String or Vec<T> directly.
Common pitfalls
1. Assuming Cow avoids all cloning
Cow only avoids cloning when the value can remain borrowed. If you call to_mut(), clone may happen immediately.
2. Returning borrowed data from temporary sources
You cannot return Cow::Borrowed from a temporary string created inside the function. The borrowed data must outlive the returned Cow.
3. Using Cow when ownership is already required
If your function stores data in a struct for long-term use, taking String may be simpler and more explicit.
4. Forgetting that Cow is generic
Cow<'a, str> is common, but Cow also works with slices and paths. The same design pattern applies across many domains.
A realistic example: parsing a header value
Suppose you are parsing a header-like value that may need trimming and lowercasing only if it contains uppercase letters or extra whitespace.
use std::borrow::Cow;
fn canonicalize_header_value(input: &str) -> Cow<'_, str> {
let trimmed = input.trim();
if trimmed == input && trimmed.chars().all(|c| !c.is_ascii_uppercase()) {
Cow::Borrowed(input)
} else {
Cow::Owned(trimmed.to_ascii_lowercase())
}
}
fn main() {
let a = canonicalize_header_value("content-type");
let b = canonicalize_header_value(" Content-Type ");
assert_eq!(a, "content-type");
assert_eq!(b, "content-type");
}This is a practical pattern for request processing, configuration parsing, and protocol adapters. The function preserves the original input when possible and allocates only when normalization is required.
Cow versus alternatives
| Approach | Pros | Cons | Best for |
|---|---|---|---|
&str | Simple, zero allocation | Cannot own or return transformed data easily | Read-only APIs |
String | Easy ownership and mutation | Forces allocation for borrowed inputs | Stored or heavily mutated text |
Cow<'a, str> | Flexible, allocates only when needed | Slightly more complex API | Conditional transformation |
impl AsRef<str> | Very ergonomic for read-only access | Not suitable when ownership may be needed | Input-only functions |
A useful rule of thumb: use AsRef for read-only access, String for always-owned data, and Cow when the function may need to return or mutate data conditionally.
Performance considerations
Cow can reduce allocations, but performance depends on the workload.
It is most effective when:
- Most inputs are already in the desired form
- Transformations are rare
- Borrowed data is common in hot paths
It may be less effective when:
- Nearly every input requires mutation
- The borrowed/owned branching adds complexity without measurable benefit
- The code becomes harder to read than a direct
Stringimplementation
Always measure if performance is the main motivation. Cow is best justified by API flexibility and predictable allocation behavior, not by assumptions.
Conclusion
Cow is one of Rust’s most practical abstraction tools for API design. It lets you accept borrowed or owned data through a single interface, preserve zero-copy behavior when possible, and allocate only when a transformation actually requires it.
Use it when your function sometimes needs ownership but often can work with borrowed input. In those cases, Cow gives you a clean balance of ergonomics, performance, and correctness.
