What Cow<'a, str> is and why it matters

Cow<'a, str> is an enum from std::borrow with two variants:

  • Cow::Borrowed(&'a str)
  • Cow::Owned(String)

The key idea is simple: if your function only needs to read or lightly transform text, it can often avoid allocating a new String. If it must modify the text, it can “upgrade” to owned storage at the point of mutation.

This is especially useful in APIs that:

  • accept either string literals or owned strings
  • conditionally normalize, trim, or rewrite text
  • return data that is usually unchanged, but sometimes transformed
  • process large volumes of text where allocations become measurable

A common mistake is to convert everything into String too early. That forces allocation even when the input is already borrowed and no mutation is needed.


When Cow<'a, str> is a good fit

Cow<'a, str> shines when the majority of values are read-only, and only a minority require allocation.

Typical examples include:

  • path-like or label-like text that may need normalization
  • configuration values that may be overridden or rewritten
  • parsed tokens that are usually passed through unchanged
  • sanitization functions that only allocate when a replacement is required

It is less useful when:

  • you always need an owned value
  • the data will outlive the input scope and must be stored long-term
  • the transformation is guaranteed to allocate anyway
  • the code becomes harder to understand than a simpler String-based design

A good rule: use Cow<'a, str> when “borrow if possible, own if necessary” is a real and frequent optimization, not just a theoretical one.


A practical example: conditional normalization

Suppose you want to normalize a user-provided identifier by trimming whitespace and converting spaces to underscores, but only if needed.

use std::borrow::Cow;

fn normalize_identifier(input: &str) -> Cow<'_, str> {
    let trimmed = input.trim();

    // Fast path: no leading/trailing whitespace and no spaces inside.
    if trimmed == input && !input.contains(' ') {
        return Cow::Borrowed(input);
    }

    let mut out = String::with_capacity(trimmed.len());
    for ch in trimmed.chars() {
        if ch == ' ' {
            out.push('_');
        } else {
            out.push(ch);
        }
    }

    Cow::Owned(out)
}

fn main() {
    let a = normalize_identifier("service_name");
    let b = normalize_identifier("  hello world  ");

    assert_eq!(a.as_ref(), "service_name");
    assert_eq!(b.as_ref(), "hello_world");
}

This function avoids allocation for the common case where the input is already valid. Only the transformed case allocates a String.

Why this is better than always returning String

If you returned String unconditionally, even "service_name" would be copied into a new heap allocation. With Cow, the caller can still treat the result like a string via as_ref() or deref coercions, but you preserve the option to stay borrowed.


Designing APIs with Cow<'a, str>

A common pattern is to accept impl Into<Cow<'a, str>> or &str depending on the API shape.

For inputs

If your function only needs to inspect the text, prefer borrowing:

fn validate_name(name: &str) -> bool {
    !name.is_empty() && name.len() <= 64
}

If the function may need to store or transform the value conditionally, Cow can be appropriate:

use std::borrow::Cow;

fn prepare_label<'a>(label: impl Into<Cow<'a, str>>) -> Cow<'a, str> {
    let label = label.into();

    if label.contains('\t') {
        Cow::Owned(label.replace('\t', " "))
    } else {
        label
    }
}

This lets callers pass either &str or String naturally.

For outputs

Returning Cow<'a, str> is most useful when the output may be either borrowed from the input or newly allocated. For example, a parser that strips optional prefixes can return a borrowed slice when no change is needed.

use std::borrow::Cow;

fn strip_prefix_if_present<'a>(s: &'a str, prefix: &str) -> Cow<'a, str> {
    if let Some(rest) = s.strip_prefix(prefix) {
        Cow::Borrowed(rest)
    } else {
        Cow::Borrowed(s)
    }
}

This example is intentionally simple: it returns borrowed data in both branches. But once you need to rewrite the string, Cow gives you a clean transition to owned storage.


Avoiding accidental allocations

Cow only helps if you preserve the borrowed path. Several patterns can accidentally force allocation too early.

1. Calling to_string() too soon

fn bad(input: &str) -> String {
    let s = input.to_string();
    if s.contains(' ') {
        s.replace(' ', "_")
    } else {
        s
    }
}

This allocates immediately, even when no change is needed.

A better approach is to inspect first, then allocate only if required.

2. Converting to String in helper functions

If a helper takes String instead of &str or Cow<'a, str>, it may force callers to allocate just to satisfy the signature.

Prefer signatures that match the actual ownership needs of the function.

3. Mutating when a borrowed return would do

Sometimes the simplest solution is to return a borrowed slice directly:

fn first_word(s: &str) -> &str {
    s.split_whitespace().next().unwrap_or("")
}

Using Cow here would add complexity without benefit. If no mutation is needed, a plain borrowed return type is usually better.


Comparing common string-return strategies

StrategyBest forAllocation behaviorNotes
&strPure borrowingNoneFastest and simplest when the output is always a slice of existing data
StringAlways-owned resultsAlwaysBest when the result must outlive inputs or be mutated heavily
Cow<'a, str>Conditional ownershipOn demandGood when most results are unchanged, but some require rewriting

The main tradeoff is complexity versus flexibility. Cow is a middle ground, not a universal replacement.


Performance considerations in real code

Cow<'a, str> can improve performance, but only in the right context. The benefits come from avoiding allocations, not from making string operations inherently faster.

Where it helps most

  • high-throughput request processing
  • log or telemetry enrichment
  • text normalization pipelines
  • parsers that preserve original text when possible
  • APIs that accept both literals and owned strings

Where it may not help

  • small, one-off CLI tools
  • code paths dominated by parsing or I/O rather than allocation
  • transformations that always allocate anyway, such as concatenation of unrelated pieces
  • code where the borrowed/owned distinction obscures intent

If your profiling shows that allocation is a bottleneck, Cow is worth evaluating. If not, keep the API simple.


A realistic normalization pipeline

Consider a service that accepts tags from multiple sources. Most tags are already valid, but some contain uppercase characters or extra whitespace. You want to normalize only when necessary.

use std::borrow::Cow;

fn normalize_tag<'a>(tag: impl Into<Cow<'a, str>>) -> Cow<'a, str> {
    let tag = tag.into();

    let trimmed = tag.trim();
    let needs_change = trimmed != tag || trimmed.chars().any(|c| c.is_uppercase());

    if !needs_change {
        return tag;
    }

    let mut out = String::with_capacity(trimmed.len());
    for ch in trimmed.chars() {
        out.push(ch.to_ascii_lowercase());
    }

    Cow::Owned(out)
}

This version preserves borrowed input when the tag is already normalized. It allocates only when trimming or case conversion is needed.

Notes on the implementation

  • trim() returns a borrowed slice, so it does not allocate.
  • The needs_change check is cheap compared with building a new string.
  • String::with_capacity(trimmed.len()) reduces reallocation during rewriting.
  • to_ascii_lowercase() is appropriate only for ASCII tags; for Unicode-aware normalization, use a more deliberate strategy.

Best practices for using Cow<'a, str>

Prefer it at API boundaries, not everywhere

Cow is most valuable in public interfaces or shared utility layers where callers may provide either borrowed or owned text. Internally, if a function always ends up owning the string, use String directly.

Keep the borrowed path obvious

The main benefit of Cow is clarity about when allocation happens. Structure code so the borrowed fast path is easy to see and reason about.

Avoid overusing Cow in deeply nested code

If every helper returns Cow, the code can become noisy. Often it is better to keep Cow at the outer boundary and convert to a simpler type internally.

Be careful with lifetimes

Cow<'a, str> borrows from some input lifetime 'a. That is powerful, but it means the borrowed result cannot outlive the source data. If you need to store the value beyond that scope, convert to String with into_owned().

Use as_ref() when you only need a &str

Cow implements AsRef<str> and deref coercions, so you can usually pass it to functions expecting &str without extra ceremony.


When to call into_owned()

Sometimes the right move is to start with Cow and later commit to ownership.

use std::borrow::Cow;

fn store_label(label: impl Into<Cow<'static, str>>) -> String {
    let label = label.into();
    label.into_owned()
}

This is useful when an API accepts flexible input but the final destination requires owned storage. The key is to delay allocation until ownership is actually required.


Common pitfalls

Returning borrowed data from a temporary

This is invalid:

use std::borrow::Cow;

fn bad() -> Cow<'static, str> {
    let s = String::from("temp");
    Cow::Borrowed(&s)
}

The borrowed reference does not live long enough. If the value must escape the function, return Cow::Owned(s) or just String.

Using Cow for every string type

Not every string-related API benefits from Cow. If the function is clearly read-only, &str is cleaner. If it clearly owns its result, String is simpler.

Forgetting to benchmark

Cow is an optimization tool, not a guarantee. Measure allocation counts and throughput before and after the change. In some cases, the extra branching or lifetime complexity is not worth the marginal gain.


A practical decision guide

Use this quick rule set:

  • Use &str when the function only reads text.
  • Use String when the function always produces or stores owned text.
  • Use Cow<'a, str> when the function often returns input unchanged, but sometimes needs to rewrite it.

That last case is where Cow earns its keep.


Learn more with useful resources