When borrowed data needs to survive mutation
You are processing a stream of log lines. Most lines are fine as-is. A few contain sensitive tokens that need masking. You write a function that takes a line, checks for the token, and returns the processed text. If the line is clean, you want to return the original slice without copying. If it has a token, you need to allocate a new buffer, mask the token, and return that.
Python handles this effortlessly because strings are just objects and the runtime manages the memory. Rust stops you. You cannot return a &str if you sometimes allocate a new String. The lifetimes do not match. You cannot return a String if you want to avoid allocating for the clean lines. You need a type that bridges the gap between borrowed and owned data.
The string types in Rust
Rust splits text into two fundamental types. &str is a borrowed slice. It is a pointer and a length. It points to text that lives somewhere else. It cannot change the text. It cannot extend the text. It just looks at it. String is an owned heap allocation. It holds the bytes. It can grow. It can shrink. It is responsible for freeing the memory.
This split is powerful. It prevents accidental copies and enforces clear ownership. It also creates friction when you need to pass text around and sometimes mutate it. Cow<'a, str> solves this. Cow stands for "Clone on Write". It is an enum with two variants. Cow::Borrowed holds a &'a str. Cow::Owned holds a String.
You can pass a Cow anywhere a &str is expected. When you need to mutate, you check if it is borrowed. If it is, you clone it into an owned string. If it is already owned, you mutate in place. The name comes from the pattern where you clone the data only when you write to it.
Treat Cow as a string until you need to mutate. The compiler handles the rest.
Minimal example
use std::borrow::Cow;
/// Returns the input as-is if it has no spaces, otherwise uppercases it.
fn process(s: &str) -> Cow<'_, str> {
if s.contains(" ") {
// We must allocate because to_uppercase creates a new String.
// Cow::Owned wraps that String.
Cow::Owned(s.to_uppercase())
} else {
// No mutation needed. Wrap the original slice.
// Zero allocation. The caller keeps ownership of the source.
Cow::Borrowed(s)
}
}
fn main() {
let input = "hello";
let result = process(input);
// result is Cow::Borrowed. No allocation happened.
println!("{}", result);
let input2 = "hello world";
let result2 = process(input2);
// result2 is Cow::Owned. A new String was allocated.
println!("{}", result2);
}
The function signature returns Cow<'_, str>. The lifetime '_ is elided and ties to the input &str. If the function returns Cow::Borrowed, the output borrows from the input. If it returns Cow::Owned, the output owns its data and the lifetime constraint is satisfied trivially.
Convention aside: use Cow::from when constructing values from variables. Cow::from("literal") creates a Cow::Borrowed. Cow::from(my_string) creates a Cow::Owned. The compiler infers the variant. This keeps code cleaner than explicit Cow::Borrowed or Cow::Owned constructors in many cases.
How Cow works under the hood
Cow is an enum. At runtime, it carries a discriminant to track whether it holds borrowed or owned data. On a 64-bit system, a &str is 16 bytes (pointer and length). A String is 24 bytes (pointer, length, capacity). Cow<'a, str> is typically 24 bytes. It matches the size of the String variant. The discriminant fits into the layout without adding extra padding.
Passing a Cow by value moves the enum. If you pass it by reference, you get a &Cow, which is just a pointer. The cost is the extra bytes on the stack when you pass it around, and the branch to check the variant. The benefit is avoiding heap allocation in the borrowed case. If 90% of your inputs do not need mutation, Cow saves you 90% of the allocations.
Cow implements Deref<Target=str>. This means you can call .len(), .chars(), .split(), and all other string methods directly on a Cow. You do not need to match on the variant to read the data. The compiler inserts the dereference automatically. This is why Cow feels like a string in practice.
Cow also implements Borrow<str>. This allows collections like HashMap to look up Cow keys using &str. The lookup works seamlessly because Cow borrows as str for hashing and equality checks.
Trust the Deref implementation. Write code that operates on str, and it will work on Cow without changes.
Realistic usage: configuration and collections
Cow shines in collections. Imagine a HashMap of configuration keys. Most keys are static strings like "host" or "port". Some keys come from user input and are dynamic. If you use String for keys, you allocate every key. If you use &str, you cannot store dynamic keys. Cow<'static, str> lets you store both. Static keys are borrowed from the binary's read-only memory. Dynamic keys are owned. The map holds a mix without forcing allocation on the static keys.
use std::borrow::Cow;
use std::collections::HashMap;
fn main() {
// Keys can be borrowed static strings or owned dynamic strings.
let mut config: HashMap<Cow<'static, str>, String> = HashMap::new();
// Static key, borrowed. No allocation for the key.
config.insert(Cow::Borrowed("host"), "localhost".to_string());
// Dynamic key, owned. Allocation for the key.
let dynamic_key = format!("custom_{}", 42);
config.insert(Cow::Owned(dynamic_key), "value".to_string());
// Lookup works with &str because Cow implements Borrow<str>.
if let Some(val) = config.get("host") {
println!("Host is {}", val);
}
}
This pattern is standard in high-performance config stores and caches. You avoid allocating keys that never change, while still supporting dynamic keys when needed.
Another common scenario is text normalization. You read a value, trim whitespace, and lowercase it. Most values are already clean.
use std::borrow::Cow;
/// Normalizes a config value. Returns borrowed if no changes are needed.
fn normalize_config(value: &str) -> Cow<'_, str> {
let trimmed = value.trim();
// If trim changed the length, we lost the original slice.
if trimmed.len() != value.len() {
return Cow::Owned(trimmed.to_lowercase());
}
// Check for uppercase without allocating.
if trimmed.chars().any(|c| c.is_uppercase()) {
return Cow::Owned(trimmed.to_lowercase());
}
// Value is clean. Return borrowed.
Cow::Borrowed(trimmed)
}
fn main() {
let raw = " API_KEY ";
let clean = normalize_config(raw);
// clean is Cow::Owned because of trim and lowercase.
assert_eq!(clean, "api_key");
let raw2 = "api_key";
let clean2 = normalize_config(raw2);
// clean2 is Cow::Borrowed. No allocation.
assert!(matches!(clean2, Cow::Borrowed(_)));
}
The function checks for mutations before allocating. It uses .chars().any() to detect uppercase characters without creating a new string. Only when a change is confirmed does it allocate. This keeps the hot path allocation-free.
If every path mutates, drop the Cow. You are paying for a bridge you never cross.
Pitfalls and performance traps
The most common trap is lifetime mismatches. Cow<'a, str> borrows data that must live for 'a. If you create a String inside a function and try to return Cow::Borrowed(&local_string), the compiler rejects it. The local string is dropped when the function returns. The borrow would dangle. You get E0515 (returns a value referencing data owned by the current function). The fix is to return Cow::Owned(local_string).
// This fails to compile.
fn bad() -> Cow<'static, str> {
let s = String::from("hello");
Cow::Borrowed(&s) // Error: s is dropped at end of function.
}
Another pitfall is assuming Cow is free. Cow has a size overhead. A &str is 16 bytes. A Cow is 24 bytes. If you pass Cow by value in a tight loop, the larger size can hurt cache performance. The enum discriminant also introduces a branch. If the branch predictor fails, the CPU stalls. Cow is only worth it when the mix of borrowed and owned data is unpredictable and allocation is expensive.
If you know the data is always owned, use String. If you know it is always borrowed, use &str. Cow is a tool for uncertainty.
Convention aside: when you need to convert a Cow to a String, use .into_owned(). It moves the string if the Cow is owned, or clones if it is borrowed. It is efficient and idiomatic. Do not manually match and clone.
Trust the borrow checker on lifetimes. If Cow::Borrowed fails, the data will not live long enough.
Decision: picking the right string type
Use &str when you only need to read text and the data lives elsewhere. Use &str for function parameters to accept both string literals and String slices without forcing the caller to allocate. Use String when you need to own the text, mutate it, or return it from a function where the source data might be dropped. Use String for building text incrementally with .push_str or .format_args. Use Cow<'a, str> when you have a function that sometimes returns borrowed data and sometimes returns owned data. Use Cow when profiling shows that allocation is a bottleneck and a significant portion of inputs can be returned without mutation. Use Cow for configuration parsing or text processing pipelines where most data passes through unchanged. Use Cow in collections like HashMap when keys are a mix of static literals and dynamic strings.