String vs &str in Rust: Key Differences Explained

Max WellsMax WellsFounder of Rustify

TL;DR: Rust has two primary string types. String is an owned, heap-allocated, growable UTF-8 string: like a Vec<u8> that is guaranteed to be valid UTF-8. &str is a borrowed string slice: a reference to a sequence of UTF-8 bytes stored somewhere (stack, heap, or the binary). Use String when you need to own or mutate a string; use &str in function parameters when you only need to read. Both are always valid UTF-8; Rust has no undefined encoding behavior.


What Is the Difference Between String and &str?

String owns its data on the heap and can grow. &str is a read-only view into string data owned by someone else; it is always a borrowed reference.

fn main() {
    // &str: string literal, stored in the program binary, 'static lifetime
    let s1: &str = "hello world";
 
    // String: owned, heap-allocated, growable
    let s2: String = String::from("hello world");
    let s3: String = "hello world".to_string();
    let s4: String = format!("hello {}", "world");
 
    // &str from String: borrow the String's contents
    let s5: &str = &s2;
    let s6: &str = &s2[0..5]; // "hello": a slice
 
    // String is mutable; &str is always read-only
    let mut owned = String::from("hello");
    owned.push_str(", world!");
    println!("{owned}"); // "hello, world!"
}

Which One Should I Use in Function Parameters?

Use &str for function parameters: it accepts both String references and &str literals without the caller needing to own a String.

// Good; accepts any string-like argument
fn greet(name: &str) {
    println!("Hello, {name}!");
}
 
// Less flexible; caller must provide an owned String
fn greet_owned(name: String) {
    println!("Hello, {name}!");
}
 
fn main() {
    greet("Alice");                           // &str literal; works
    greet(&String::from("Bob"));              // &String coerces to &str; works
 
    greet_owned(String::from("Charlie"));     // works
    // greet_owned("Dave");                   // ERROR: expected String, got &str
}

The rule of thumb: accept &str, return String when writing functions. Use String in structs when the struct needs to own its data. Use &str in structs only when the struct borrows from data that outlives it (requires lifetime annotations).


How Do You Convert Between String and &str?

&strString via .to_string() or String::from(). String&str via & (deref coercion) or as_str().

fn main() {
    // &str → String (allocates)
    let owned: String = "hello".to_string();
    let owned2: String = String::from("hello");
    let owned3: String = "hello".to_owned();
 
    // String → &str (zero-cost borrow)
    let borrowed: &str = &owned;
    let borrowed2: &str = owned.as_str();
    let slice: &str = &owned[1..4]; // "ell"
 
    // Concatenation: + operator consumes the left String
    let s1 = String::from("hello, ");
    let s2 = String::from("world!");
    let combined = s1 + &s2; // s1 is moved here; s2 is borrowed
    // s1 is no longer valid; s2 still is
 
    // format!: does not consume anything
    let s1 = String::from("hello, ");
    let combined2 = format!("{s1}{s2}");
}

Why Does Rust Have Two String Types?

The split between owned (String) and borrowed (&str) is a direct consequence of Rust's ownership model; it allows Rust to avoid unnecessary heap allocations.

In languages with garbage collection (Python, Java, Go), strings are always heap objects; every string literal creates an allocation. In Rust:

  • String literals ("hello") are embedded in the compiled binary and have 'static lifetime: no allocation.
  • &str can reference binary data, stack data, or heap data: no copy, no allocation.
  • String is allocated only when you need ownership or mutation.

This means parsing a large file and extracting substrings can be done entirely with &str slices: zero copies while a GC language would allocate a new string for each substring.


How Does Rust Handle Non-ASCII and Unicode?

Rust strings are always valid UTF-8. Indexing by byte position is supported; indexing by character is not (because characters have variable byte width).

fn main() {
    let s = "héllo"; // UTF-8: 'é' is 2 bytes
 
    println!("{}", s.len());        // 6 (bytes, not chars)
    println!("{}", s.chars().count()); // 5 (Unicode scalar values)
 
    // Iterate over characters safely
    for c in s.chars() {
        print!("{c} "); // h é l l o
    }
 
    // Byte slicing: must be on valid UTF-8 boundaries
    let hello = &s[0..1]; // "h": safe, 1-byte char
    // let bad = &s[0..2]; // PANIC at runtime: 'é' is 2 bytes, cut in half
 
    // Safe char-based slicing
    let first_two: String = s.chars().take(2).collect(); // "hé"
}

To work with individual bytes: s.as_bytes() returns &[u8]. To index by grapheme clusters (user-perceived characters), use the unicode-segmentation crate.


Frequently Asked Questions

&String is a reference to an owned String. &str is a string slice. Rust's deref coercion automatically converts &String to &str in most contexts; functions accepting &str work transparently with &String arguments. Prefer &str in function signatures for maximum flexibility.

str is an unsized type: a dynamically-sized sequence of UTF-8 bytes. It cannot exist on its own because the compiler doesn't know its size. You always use it behind a reference (&str) or a box (Box<str>). In practice, you almost never write str directly.

Box<str> is a heap-allocated, immutable string slice: slightly smaller than String (no capacity field). Use it when you have an immutable string that needs ownership but will never grow. Rc<str> and Arc<str> enable shared ownership of string data with reference counting.

String indexing by integer is disabled because UTF-8 characters vary in byte width: s[0] would return a byte, not a character, and could cut a multi-byte character in half. Use .chars().nth(0) for characters or .as_bytes()[0] for raw bytes.


Sources


  • Ownership: String is owned; &str is always borrowed
  • Borrow Checker: Enforces that &str references don't outlive their data
  • Lifetime: &str in structs requires lifetime annotations
  • Generic: String implements many generic traits like From<&str>
  • Regex: Regex engines mostly operate on &str and String input
  • Reference: String vs &str is fundamentally an ownership and reference distinction

Keep Reading

Ready to Land a $120k+ Rust Job in the US or Europe?