Rust String vs str
debt(d3/e2/b3/t5)
Closest to 'default linter catches the common case' (d3), clippy lints `&String` parameters (ptr_arg) and the `&(mut )?String` code_pattern is machine-detectable; but the UTF-8 byte-slicing panic is silent until runtime, so it sits at the milder end of d3.
Closest to 'one-line patch or single-call swap' (e1), the quick_fix is swapping a `&String` parameter for `&str` or dropping a needless `.to_string()`; slightly above e1 because returning a borrowed slice from a local sometimes forces a small signature/ownership change, nudging toward e3.
Closest to 'localised tax' (b3), the choice applies across all Rust contexts (library/cli/web/queue) but each function's owned-vs-borrowed decision is local; a load-bearing API type signature can ripple to callers, keeping it at b3 rather than b1.
Closest to 'notable trap' (t5), the misconception that `String` and `&str` are interchangeable synonyms is the documented gotcha every Rust beginner eventually learns, and byte-index slicing panicking inside a UTF-8 char reinforces the t5 surprise level.
Also Known As
TL;DR
Explanation
Rust has two core string types and beginners routinely confuse them. `String` is an owned, heap-allocated, growable buffer of UTF-8 bytes. It owns its data, can be mutated and extended (`push_str`, `push`, `+`), and is freed when it goes out of scope. `str` is the primitive string slice type: an immutable, dynamically-sized sequence of UTF-8 bytes. You almost never handle a bare `str` by value because its size is not known at compile time; instead you work with `&str`, a fat pointer holding an address and a length that borrows into some existing UTF-8 data.
The key relationship is ownership versus borrowing. A `String` owns a heap allocation. A `&str` borrows a window into UTF-8 bytes that may live on the heap (inside a `String`), in the read-only data section of your binary (a string literal like `"hello"` has type `&'static str`), or anywhere else. Because `String` implements `Deref<Target = str>`, you can pass a `&String` almost anywhere a `&str` is expected, and `&my_string[..]` or `my_string.as_str()` produces a `&str` explicitly. This is why idiomatic function signatures take `&str` rather than `&String`: the slice accepts both owned strings and literals, so callers are not forced to allocate.
Choosing between them follows a simple rule. If a function only needs to read string data, accept `&str`. If it needs to own, store, or grow the string, take or return `String`. Returning `&str` from a function requires a lifetime tied to some input, since the borrow must not outlive the data it points into. Converting is cheap in one direction and allocating in the other: `&str` to `String` copies bytes with `.to_string()` or `.to_owned()`; `String` to `&str` is a free borrow.
Understanding this split unlocks Rust's whole string ecosystem: `format!` produces `String`, string literals are `&str`, `Cow<str>` lets you defer the choice, and slicing must always fall on UTF-8 character boundaries or it panics. Getting the types right avoids needless allocations and lifetime headaches while keeping APIs flexible.
Common Misconception
Why It Matters
Common Mistakes
- Writing function parameters as `&String` instead of `&str`, forcing callers to allocate a String just to pass a literal.
- Calling `.to_string()` or `.clone()` on data that only needs to be read, adding an allocation the borrow checker did not require.
- Trying to store a `&str` in a struct without a lifetime annotation, expecting it to keep its data alive like a `String` would.
- Slicing a string with byte indices that fall inside a multi-byte UTF-8 character, causing a runtime panic.
- Returning a `&str` that borrows from a local `String`, which is dropped at the end of the function.
Avoid When
- You genuinely need to own, grow, or mutate the string and store it beyond the current scope, where `String` is the correct choice.
- A trait or API contract requires an owned `String` return type and borrowing would leak an internal lifetime.
- You are building a string incrementally with repeated pushes, where an owned `String` buffer is the right tool.
When To Use
- Accepting string parameters that a function only reads, using `&str` so literals and owned strings both work without allocation.
- Returning a substring that borrows from an input, tying the returned `&str` to the caller's lifetime.
- Deferring the owned-versus-borrowed decision with `Cow<str>` when a value is sometimes copied and sometimes referenced.
- Storing string literals as `&'static str` constants that live for the whole program with no heap allocation.
Code Examples
// Forces every caller to own a String, even for a literal.
fn greet(name: &String) -> String {
// Unnecessary clone: we only read `name`.
let owned = name.clone();
format!("Hello, {}", owned)
}
// Returns a slice borrowing from a local String that is dropped -
// this does not compile.
fn first_word(input: String) -> &str {
let trimmed = input.trim();
trimmed.split(' ').next().unwrap()
}
fn main() {
// Must allocate a String just to call greet.
let msg = greet(&"world".to_string());
println!("{}", msg);
}
// Accepts &str, so both String and literals work with no allocation.
fn greet(name: &str) -> String {
format!("Hello, {}", name)
}
// Borrow tied to the input's lifetime - the caller owns the data.
fn first_word(input: &str) -> &str {
input.split(' ').next().unwrap_or("")
}
fn main() {
// Works with a string literal (&'static str).
println!("{}", greet("world"));
// Works with an owned String via Deref coercion.
let owned = String::from("Rustacean");
println!("{}", greet(&owned));
// Borrowed slice lives as long as `sentence`.
let sentence = String::from("the quick brown fox");
println!("{}", first_word(&sentence));
}