How to Use the Cow (Clone on Write) Pattern Effectively

Use the `.clone()` method to create an independent copy of a value when you need to retain ownership of the original while passing a copy to another scope. This is essential for types like `String` that do not implement the `Copy` trait.

When copying feels wasteful

You are building a text processor. It reads a line from a file, checks if it needs normalization, and passes it downstream. Most lines are already clean. A few contain trailing whitespace or mixed casing. If you unconditionally clone every line into a String, you pay heap allocation costs for data you never touch. If you keep everything as &str, you cannot modify the dirty lines without borrowing from the original buffer, which quickly trips the borrow checker.

Rust gives you a middle ground. The Cow type lets you accept borrowed data, work with it as if it were owned, and only allocate when a mutation actually happens. The name stands for Clone on Write. It is an enum that wraps either a reference or an owned value, and it exposes the same API for both branches.

What Cow actually does

Cow<'a, B> is a generic enum defined in std::borrow. The lifetime 'a tracks how long the borrowed branch is valid. The type parameter B must implement the ToOwned trait. That trait bridges the gap between a borrowed type and its owned counterpart. For &str, the owned type is String. For &[T], the owned type is Vec<T>.

Think of it like a shared office whiteboard. You can read the notes directly from the wall. That is the borrowed branch. Zero cost, instant access. If you need to erase and rewrite a section, you cannot deface the wall. You grab a fresh sheet, copy the relevant part, and edit that. That is the owned branch. The API you use to read or write does not change. The underlying storage does.

The compiler guarantees that the borrowed branch never outlives the data it points to. The owned branch owns its memory and cleans it up when dropped. You get the performance of references for read-only paths, and the flexibility of owned values when mutation occurs.

The minimal example

Here is the smallest working pattern. It shows how Cow holds both states and how .to_mut() triggers the clone.

use std::borrow::Cow;

/// Normalizes a string slice by trimming whitespace.
/// Returns a Cow to avoid cloning when the input is already clean.
fn normalize(input: &str) -> Cow<'_, str> {
    // Check if the string actually needs modification.
    if input.trim() == input {
        // Return the original slice without allocation.
        Cow::Borrowed(input)
    } else {
        // Allocate a new String only when mutation is required.
        Cow::Owned(input.trim().to_string())
    }
}

fn main() {
    let raw = "  hello  ";
    let clean = normalize(raw);
    
    // to_mut() returns &mut String or &mut str depending on the variant.
    // It clones the borrowed data if we are currently holding a reference.
    let mut editable = clean.to_mut();
    editable.push_str(" world");
    
    println!("{}", editable);
}

The function signature accepts &str and returns Cow<'_, str>. The underscore in the lifetime position tells the compiler to infer the lifetime from the input. When the input is already trimmed, we return Cow::Borrowed. No heap allocation occurs. When the input needs trimming, we return Cow::Owned. The String lives on the heap and will be freed when the Cow drops.

Call .to_mut() on the result to get a mutable reference. If the Cow holds a borrowed slice, it clones the data into an owned String behind the scenes. If it already owns the data, it simply hands back a mutable reference to the existing allocation.

Do not treat Cow as a magic performance fix. It is a type-level contract that defers allocation until mutation.

How the compiler sees it

Under the hood, Cow is a simple two-variant enum. The compiler lays it out as a tagged union. One byte holds the discriminant. The rest holds either a fat pointer (for slices) or a heap pointer plus length and capacity (for owned types). The size matches the larger variant, which is usually the owned type.

When you pass a Cow to a function, the compiler sees a single concrete type. You do not need separate overloads for &str and String. The trait bounds handle the divergence. Any method that works on &B also works on &Cow<B> because Cow implements Deref<Target = B>. This means you can call .len(), .chars(), or .contains() directly on the Cow without unwrapping it.

The ToOwned trait is the glue. It provides the .to_owned() method that creates the owned variant from the borrowed one. The standard library implements it for all the common slice types. You can implement it for your own types if you need custom Cow behavior, but you rarely do.

The zero-cost promise holds because the borrowed branch is literally just a reference. The compiler generates the exact same machine code as if you had passed &str directly. The owned branch pays the standard allocation cost. The branching logic happens at runtime, but the type system guarantees memory safety at compile time.

Treat Cow as a transparent wrapper. The compiler erases the enum boundary for read-only operations.

A realistic scenario

Configuration parsers often face the Cow dilemma. You read a JSON file or a TOML table. Some fields are static defaults. Some fields come from user input that might need escaping or normalization. You want to store everything in a struct, but you do not want to clone the defaults.

use std::borrow::Cow;

/// Holds application configuration with lazy allocation.
struct AppConfig<'a> {
    /// Database host. Borrowed if using the default, owned if overridden.
    db_host: Cow<'a, str>,
    /// Log level. Always owned because it gets validated and transformed.
    log_level: String,
}

impl<'a> AppConfig<'a> {
    /// Constructs config from raw string slices.
    /// Returns owned values only when transformation is necessary.
    fn new(host: &'a str, level: &str) -> Self {
        let db_host = if host.is_empty() {
            // Return a borrowed default to avoid heap allocation.
            Cow::Borrowed("localhost:5432")
        } else {
            // User provided a value. Store it as owned.
            Cow::Owned(host.to_string())
        };

        let log_level = if level == "verbose" {
            String::from("DEBUG")
        } else {
            String::from("INFO")
        };

        AppConfig { db_host, log_level }
    }
}

fn main() {
    let config = AppConfig::new("", "verbose");
    println!("Host: {}, Level: {}", config.db_host, config.log_level);
}

The struct carries a lifetime parameter because db_host might borrow from the input. If you pass an empty string, the Cow borrows a string literal that lives for the entire program. No allocation occurs. If you pass a user-supplied string, the Cow owns a String that will be freed when config drops.

This pattern shines in APIs that process streams of data. You accept &str, return Cow<'_, str>, and let the caller decide whether to store the borrowed slice or clone it. The convention in the Rust ecosystem is to use Cow in return types when the function might or might not allocate. You rarely see Cow in function parameters. Callers prefer to pass concrete types and let the callee handle the branching.

Keep the lifetime parameter explicit in structs that hold Cow. Hiding it behind inference makes the API harder to use.

Where things go wrong

The most common mistake is trying to return a Cow that outlives the input data. The compiler will reject it with E0597 (borrowed value does not live long enough). The Cow enum ties the borrowed branch to the input lifetime. If you try to store the Cow in a struct that lives longer than the input, the borrow checker blocks you.

fn bad_example(input: &str) -> String {
    let cow = std::borrow::Cow::Borrowed(input);
    // Trying to convert a borrowed Cow to a String works,
    // but holding the Cow beyond the input scope fails.
    cow.into_owned()
}

The fix is straightforward. Call .into_owned() if you need a String that outlives the input. That method consumes the Cow and returns an owned String, cloning the data if it was borrowed. The compiler error E0308 (mismatched types) appears if you try to assign a Cow<'a, str> to a String variable without conversion.

Another pitfall is calling .to_mut() inside a hot loop. That method clones the data on the first call, but it also hands back a mutable reference. If you call it repeatedly, you pay the clone cost once, which is fine. The real issue is when you accidentally mutate a borrowed Cow without realizing it triggers an allocation. Profile your code. If the mutation path is rare, Cow saves you. If half your inputs need mutation, just use String from the start.

The borrow checker will also complain with E0502 if you try to borrow a Cow immutably while holding a mutable reference from .to_mut(). This is standard Rust borrowing. Drop the mutable reference before reading again.

Do not fight the lifetime system. Convert to owned when the scope changes.

When to reach for Cow

Use &str when you only read the data and the input lifetime matches your usage scope. Use String when you know you will mutate the data or need to store it beyond the input lifetime. Use Cow<'a, str> when your function accepts borrowed data, performs conditional transformation, and wants to avoid allocation on the read-only path. Use Vec<T> when you need a fully owned, mutable collection regardless of input source. Use Cow<'a, [T]> when you work with byte slices or arrays that might need partial mutation without full reallocation. Reach for plain references when lifetimes are simple; the Cow alternative adds enum overhead that rarely pays off for trivial cases.

Where to go next