The ownership contract
You write a function to log a user message. You pass a string literal. The compiler accepts it. You try passing a String variable. The compiler rejects it with a "mismatched types" error. You add an ampersand. It works. You try passing that same String to another function that takes ownership. The compiler screams E0382 (use of moved value).
The mental model is broken. You have two types that look identical in the output but behave differently in the code. One owns the text. The other borrows it. Rust forces you to choose between String and &str at every boundary. Getting this wrong leads to allocation storms or lifetime errors. Getting it right makes your code fast and flexible.
String owns the heap. &str points to bytes.
String is a heap-allocated, growable, UTF-8 encoded string. You own the data. You can append characters, replace substrings, or clear the buffer. When the String goes out of scope, Rust frees the memory.
&str is a borrowed reference to a UTF-8 string. It is a slice. You do not own the data. You can read it, but you cannot modify it. The &str points to bytes that live somewhere else. That somewhere could be a String, a string literal embedded in the binary, or a buffer read from a file. The &str must not outlive the data it points to.
Think of String as a canvas you hold in your hands. You can paint on it, tear it up, or hand it to a friend who takes it from you. Think of &str as a photograph of that canvas. You can look at the photo, but you cannot change the painting. If the canvas burns, the photo becomes a picture of nothing.
Minimal example
/// Demonstrates the difference between owned String and borrowed &str.
fn main() {
// String allocates on the heap. It owns the bytes.
let owned = String::from("hello");
// &str borrows data. Here it points to a literal in the binary.
let borrowed: &str = "world";
// Both print the same way, but their lifetimes differ.
println!("{owned}, {borrowed}");
}
The convention here is explicit. String::from("...") creates an owned string from a literal. The community prefers this over "..." .to_string() for literals because String::from can sometimes be optimized by the compiler to avoid a dynamic dispatch, though the difference is negligible in practice. Use to_string() when converting other types that implement Display.
Under the hood: pointers and capacity
The difference shows up in memory layout. String is a struct with three fields: a pointer to the heap, the current length, and the allocated capacity. The capacity is the amount of space reserved for future growth. This allows String to append characters without reallocating every time.
&str is a fat pointer with two fields: a pointer and a length. It has no capacity. It cannot grow. It describes a contiguous sequence of UTF-8 bytes.
When you slice a String, you get a &str. The slice points to a subset of the original bytes. No copy happens. The String keeps the full allocation. The &str just points to a range.
/// Shows slicing a String to get a &str without copying.
fn main() {
// String with capacity for growth.
let full = String::from("Rust is safe");
// Slice returns &str pointing into the String.
let slice: &str = &full[0..4];
// slice points to "Rust". No allocation occurred.
println!("{}", slice);
}
Slicing is zero-cost. This is why &str is the preferred type for passing text around. You can pass a slice of a large buffer without copying the buffer. The caller keeps ownership. The callee gets a view.
Realistic example: designing function signatures
Function signatures reveal the design intent. If a function takes String, it demands ownership. The caller cannot use the String after the call. If a function takes &str, it accepts a view. The caller keeps ownership. The function can accept a String, a literal, or a slice.
/// Processes a message. Takes &str to accept both String and literals.
fn process_message(msg: &str) {
// We only read the message. We don't need to own it.
println!("Processing: {}", msg);
}
fn main() {
// Owned string.
let owned = String::from("Important data");
// String literal.
let literal = "Quick note";
// Both work because &str accepts a reference to String.
process_message(&owned);
process_message(literal);
// owned is still usable here. We only lent it.
println!("Still have: {}", owned);
}
The compiler allows process_message(&owned) because Rust automatically coerces &String to &str. This is called deref coercion. The String implements Deref<Target = str>. When the function expects &str, the compiler inserts the dereference for you.
If you change the signature to fn process_message(msg: String), the call process_message(literal) fails. The compiler cannot turn a literal into a String automatically. You must write process_message(String::from(literal)). This forces an allocation. The caller pays the cost.
Prefer &str in function arguments. It costs nothing and accepts everything. It pushes allocation decisions to the caller.
The conversion dance
Moving between String and &str is common. The direction matters for performance.
Turning a String into a &str is free. You just take a reference. The String stays alive. The &str borrows it.
Turning a &str into a String allocates. You must copy the bytes to the heap. The new String owns the data. The original &str can point to a literal or a different buffer. The copy is independent.
/// Demonstrates conversion costs between String and &str.
fn main() {
// &str to String allocates.
let owned: String = "hello".to_string();
// String to &str is a reference. No copy.
let borrowed: &str = &owned;
// borrowed points to owned. owned must outlive borrowed.
println!("{}", borrowed);
}
The convention is to use &s to get a &str from a String. This is explicit and clear. Avoid s.as_str() unless you are calling a method that specifically requires as_str() for disambiguation, which is rare. The reference syntax is idiomatic.
When you need to return text from a function, return String if you computed it locally. Returning &str from a function that creates data is a trap. The local data dies when the function ends. The &str would point to garbage.
Pitfalls and compiler errors
The borrow checker blocks dangerous patterns. You will hit these errors often when learning.
Dangling references. If you create a String inside a function and try to return a &str pointing to it, the compiler rejects you with E0515 (cannot return value referencing local variable). The String is dropped at the end of the function. The &str would be invalid.
/// This code does not compile.
fn bad() -> &str {
let s = String::from("hello");
&s // Error: returns a reference to data owned by the current function
}
The fix is to return the String. The caller owns the result. Or return a literal if the data is static.
Mutability. You cannot mutate a &str. If you try to call a method that requires &mut self, the compiler blocks you. You might see E0596 (cannot borrow as mutable) if you try to mutate through an immutable reference, or a trait bound error if the method requires mutability.
/// This code does not compile.
fn main() {
let s: &str = "hello";
// Error: &str does not implement push_str.
// s.push_str(" world");
}
&str is immutable by design. If you need to modify text, you need a String or a &mut String. The compiler enforces this. You cannot accidentally corrupt a shared view.
Indexing. Neither String nor &str supports indexing by integer. s[0] fails with E0608 (cannot index into a String). Strings are UTF-8. Bytes are not characters. Indexing by byte offset can split a multi-byte character. Rust forces you to use .chars() or .get() to access characters safely.
/// Shows safe character access.
fn main() {
let s = String::from("café");
// Indexing fails.
// let c = s[0];
// Use chars iterator for characters.
let first = s.chars().next().unwrap();
println!("{}", first);
}
The compiler protects you from invalid UTF-8 sequences and split characters. Trust the error. Reach for .chars() or slicing by character boundaries.
Decision matrix
Use String when you need to modify the text, append characters, or build a string from parts. Use String when you must return text from a function that computes it locally. Use String when you are storing text in a data structure that requires ownership. Use &str when you only need to read the text and don't care who owns the underlying bytes. Use &str as function parameters to accept both String and string literals without forcing the caller to allocate. Use &str when slicing text to avoid copying.
Don't return a view of a scroll you're about to burn. Prefer &str in arguments. It's the polite interface.