How to convert between String and str

You convert a `String` to a `&str` by borrowing it with `&` or `&s[..]`, while converting a `&str` to a `String` requires allocation using `.to_string()` or `String::from()`.

The string split

You are writing a function to validate a username. You have the username coming from a web request as a String. You also have a list of reserved names stored as string literals in your code. You write the validator to accept &str so it can handle both. You pass the String with &username, and it compiles. You pass the literal "admin", and it compiles. You feel good.

Then you try to store the validated username in a struct. You define the struct field as &str. The compiler rejects you. You change the field to String. The compiler accepts it, but now you're allocating memory for every username you store, even if you could have just pointed to the data you already have. You realize String and &str are not interchangeable. They represent different relationships to the data, and the compiler enforces that distinction strictly.

Rust splits string handling into two types to separate ownership from viewing. This split eliminates a whole class of bugs related to dangling pointers and accidental copies, but it requires you to think about who owns the bytes and who is just looking at them.

Ownership vs view

String is an owned, growable, heap-allocated buffer. It owns the bytes. When the String goes out of scope, the memory is freed. String lives on the heap, and the variable on the stack holds a pointer, a length, and a capacity.

&str is a borrowed view of string data. It does not own the bytes. It is a "fat pointer" consisting of a pointer to the data and a length. &str can point to data on the heap (inside a String) or data in the binary's static section (a string literal). &str is always immutable. You cannot change the bytes through a &str.

Think of String as a whiteboard you bought. You can write on it, erase it, and expand it. &str is a window into that whiteboard. You can see what's written. You can measure how much text is there. You cannot write on the whiteboard through the window. The window can look at your whiteboard or a poster on the wall. The window doesn't own the surface it's looking at. If the whiteboard gets thrown away, the window shows nothing.

This distinction forces you to be explicit about memory management. If you need to modify the text, you must have a String. If you only need to read it, you can use a &str, which costs nothing to create and pass around.

Borrowing a String

Converting a String to a &str is a zero-cost operation. You are not copying data. You are creating a view. The compiler generates a fat pointer pointing to the same heap allocation the String uses.

fn main() {
    // String allocates on the heap.
    let owned = String::from("Rust is fast");

    // Borrowing creates a &str view.
    // No allocation. No copy. Just a pointer and length.
    let view: &str = &owned;

    println!("View: {view}");
    println!("Owned: {owned}");
}

The syntax &owned works because String implements Deref<Target = str>. This trait tells the compiler that a reference to a String can be treated as a reference to a str. You can also use slice syntax &owned[..], which does the exact same thing. The community convention is to use &owned. The slice syntax adds visual noise without adding clarity. Stick to &owned unless you are slicing a substring.

Borrowing ties the lifetime of the &str to the String. The &str cannot outlive the String. If you try to return a &str that points to a local String, the compiler rejects you. The data would be dropped before the view could be used.

fn bad_function() -> &str {
    let s = String::from("temporary");
    &s // Error: returns a value referencing data owned by the current function
}

The compiler protects you from dangling pointers. The view is only valid while the owner exists. Trust this guarantee. It is the foundation of Rust's memory safety.

Allocating a String

Converting a &str to a String requires allocation. The String must own the data, so it requests memory from the heap, copies the bytes from the &str into that memory, and sets the length and capacity. This operation has a cost. It involves a system call or a heap manager interaction, and it copies bytes.

Use .to_string() for the idiomatic conversion. It is a method on &str that returns a String. You can also use String::from(&str), which does the same thing. The convention is to prefer .to_string() for readability. It reads like natural language: "take this slice and make it a string." String::from is explicit about construction, which some developers prefer when chaining operations, but .to_string() is the standard choice in most codebases.

fn main() {
    let view: &str = "Hello, world";

    // Allocates a new String on the heap.
    // Copies the bytes from the static data.
    let owned: String = view.to_string();

    println!("Owned: {owned}");
    // view is still valid. It points to static data.
}

Allocation is not free. In performance-critical loops, converting &str to String repeatedly can become a bottleneck. Profile your code. If you see allocation spikes in string processing, look for ways to keep data as &str longer. Pass references instead of owned values. Use stack-allocated buffers when possible. Allocation is the enemy of latency. Borrow to win.

The magic of Deref

The Deref trait is the bridge between String and &str. String implements Deref<Target = str>. This implementation enables automatic deref coercion. When the compiler sees a &String where a &str is expected, it automatically inserts a deref call to convert the reference.

This coercion is why functions accepting &str can take both String and string literals. The caller passes &s for a String, and the compiler coerces it to &str. The caller passes a literal, which is already a &str, and it matches directly.

/// Prints any string data without taking ownership.
/// Accepts &str, which coerces from &String.
fn greet(name: &str) {
    println!("Hello, {name}!");
}

fn main() {
    let owned = String::from("Alice");
    greet(&owned); // &String coerced to &str

    greet("Bob"); // &str literal matches directly
}

Deref coercion also allows you to call &str methods on a String. Methods like .len(), .is_empty(), and .contains() are defined on str. Because of Deref, you can call owned.len() on a String. The compiler rewrites this as (*owned).len(), which becomes str::len(&owned).

You cannot call String methods on a &str. Methods like .push_str() and .truncate() require ownership. They mutate the buffer. A &str has no buffer to mutate. If you try to call .push_str() on a &str, the compiler rejects you with a method not found error. The type system enforces the boundary. You need a String to modify text.

Real-world pattern

In real code, you will often see structs that store String for owned data and functions that accept &str for flexibility. This pattern minimizes allocation while maintaining clear ownership.

Consider a configuration loader. The config data comes from a file as a String. You parse it and extract values. You store the values in a struct. If the values are short-lived, you might pass them as &str to processing functions. If you need to store them, you convert to String.

/// Stores application configuration.
/// Fields are String because the struct owns the data.
struct Config {
    app_name: String,
    version: String,
}

/// Parses a raw config string into a Config struct.
/// Takes &str to avoid forcing the caller to allocate.
fn parse_config(raw: &str) -> Config {
    // In a real app, you'd use a parser.
    // Here we simulate extraction.
    let app_name = "MyApp";
    let version = "1.0.0";

    Config {
        // Convert &str to String for storage.
        // Allocation happens here, once per config load.
        app_name: app_name.to_string(),
        version: version.to_string(),
    }
}

fn main() {
    // Simulate reading from a file.
    let raw_data = String::from("app_name=MyApp\nversion=1.0.0");

    // Pass &str to parser. No extra allocation.
    let config = parse_config(&raw_data);

    println!("App: {}, Version: {}", config.app_name, config.version);
}

This pattern keeps allocation localized. The parser accepts a view. The struct owns the data. The caller passes a view of the raw data. You avoid copying the raw data into the parser. You only allocate when you need to store the result. This is efficient and idiomatic.

Pitfalls

The boundary between String and &str causes common errors. Understanding the compiler messages helps you fix them quickly.

Mismatched types. If you pass a String where a &str is expected without borrowing, the compiler rejects you with E0308 (mismatched types). The compiler sees an owned value where a reference is needed. Add & to borrow the String.

fn print_text(t: &str) {
    println!("{t}");
}

fn main() {
    let s = String::from("text");
    print_text(s); // Error E0308: expected &str, found String
    print_text(&s); // Fixed: borrowed the String
}

Borrow conflicts. If you borrow a String as &str and then try to mutate the String, the compiler rejects you with E0502 (cannot borrow as mutable because it is also borrowed as immutable). The mutable borrow would invalidate the immutable view. Drop the view before mutating.

fn main() {
    let mut s = String::from("hello");
    let view = &s; // Immutable borrow
    s.push_str(" world"); // Error E0502: cannot borrow s as mutable
    println!("{view}");
}

Lifetime leaks. If you try to return a &str from a function that creates a String locally, the compiler rejects you. The error message says "returns a value referencing data owned by the current function." The String is dropped at the end of the function. The &str would point to freed memory. Return a String instead, or restructure the code to pass the data in.

Storing &str in structs. If you store &str in a struct, the struct must have a lifetime parameter. This propagates lifetimes through your codebase. It can become complex quickly. If you are unsure about lifetimes, store String. It is easier to work with, and the allocation cost is often negligible compared to the complexity of lifetime annotations. Use String in structs unless you have a measured reason to avoid allocation.

Decision matrix

Use &str for function parameters when you only need to read the text. It accepts both String and literals via coercion, and it avoids forcing the caller to allocate.

Use String for struct fields when you need to store the data. It owns the bytes, so the struct can outlive the source data. It simplifies lifetime management.

Use String when you need to modify the text. Methods like .push_str(), .replace(), and .truncate() require ownership. You cannot mutate through a &str.

Use &str for string literals. Literals are &str by default. They live in static memory. Converting them to String allocates unnecessarily unless you need to modify them.

Use Cow<str> when writing a library function that accepts either &str or String and wants to avoid allocation on &str input. Cow stands for "clone on write." It holds either a borrowed &str or an owned String. If the caller passes &str, Cow stores the reference. If you need to modify the data, Cow clones it into a String automatically. This pattern is useful for APIs that want to be flexible without penalizing performance.

Don't allocate unless you have to. Borrow first. Allocate only when you must. The compiler guides you toward efficient code if you listen to it.

Where to go next