How to Convert Between String and &str in Rust

You convert a `String` to `&str` using the `as_str()` method or by simply borrowing it, while converting `&str` to `String` requires calling the `to_string()` method or using `String::from()`.

The ownership trap

You write a function to validate an email address. You test it with a hardcoded literal like "user@example.com" and it works. You try to pass a String you just read from a database, and the compiler rejects you. You add an ampersand, it compiles. You try to store that email in a struct, and the compiler screams about lifetimes. You convert the email to a String, and it finally works.

You just danced between String and &str. You didn't realize it, but you made a choice about memory ownership every time. Rust splits strings into two types to force you to think about who owns the bytes. String owns the data. &str is a view. Converting between them is the difference between taking a book home and checking out a library card.

String owns. &str views.

String is a growable, heap-allocated buffer. It owns the bytes. When the String goes out of scope, Rust frees the memory. &str is a reference to a sequence of UTF-8 bytes. It never owns data. The bytes might live inside a String, or they might be hardcoded in the binary as a string literal. &str is just a pointer to the start of the text and its length.

Think of String as a house you bought. You can paint the walls, knock down a room, or sell the house. You control the structure. &str is a photograph of the house. The photograph shows you what's there, but you can't change the house through the photo. If someone demolishes the house, the photograph still exists, but the thing it points to is gone. Rust refuses to let you hand someone a photograph of a house that's about to be destroyed.

The memory layout

Understanding the memory layout explains why conversions work the way they do. A String stores three pieces of metadata: a pointer to the heap data, the current length, and the allocated capacity. The capacity exists because String might grow. If you append text, Rust uses the extra capacity to avoid reallocating immediately.

A &str stores only two pieces: a pointer and a length. It has no capacity. It cannot grow. It is a fixed window into UTF-8 bytes.

This layout makes the direction of conversion obvious. Turning a String into a &str is cheap. You just take the pointer and length from the String and wrap them in a reference. You ignore the capacity. No memory moves. No allocation happens.

Turning a &str into a String is expensive. You have to ask the allocator for a new chunk of memory. You copy every byte from the source into the new buffer. You build a new header with the pointer, length, and capacity. This is a real allocation. It takes time. It can fail if the system is out of memory.

Borrowing is free

When you have a String and need a &str, you are borrowing. The operation is zero-cost in terms of allocation. You can do this explicitly with as_str(), or you can rely on Rust's coercion rules.

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

    // as_str extracts the pointer and length.
    // No heap allocation occurs here.
    let explicit_view: &str = owned.as_str();

    // The compiler also coerces &String to &str automatically.
    // This uses the Deref trait under the hood.
    let implicit_view: &str = &owned;

    // Both views point to the same bytes.
    assert_eq!(explicit_view, implicit_view);
}

The Deref trait is the magic here. String implements Deref<Target=str>. This tells the compiler that whenever a &str is expected, a &String can be automatically converted. You rarely need to call as_str() manually. The community convention is to use &owned and let the compiler handle the coercion. Use as_str() only when you need to make the intent explicit for readers, or when you are chaining methods and the compiler gets confused.

Borrowing is free. Reach for references whenever you can.

Owning costs allocation

When you have a &str and need a String, you must take ownership. This requires cloning the bytes into a new heap buffer. You have two ways to do this: to_string() and String::from(). Both produce identical code. Both allocate. Both copy.

fn main() {
    // String literals are &str.
    // They live in the binary, not on the heap.
    let literal: &str = "Static data";

    // to_string allocates a new String and copies the bytes.
    // This is the community convention for readability.
    let owned_a: String = literal.to_string();

    // String::from does the exact same thing.
    // It is less common but semantically identical.
    let owned_b: String = String::from(literal);

    // Both own independent copies of the data.
    println!("A: {}, B: {}", owned_a, owned_b);
}

The convention is clear. Use to_string(). It reads left-to-right like English: "take this value and make it a string". String::from() puts the type first, which is useful in some generic contexts, but for simple conversion, to_string() is the standard.

Allocation is not free. If you are in a tight loop converting &str to String, you are hammering the allocator. Profile first. Often you can refactor the code to accept &str instead.

The coercion magic

Rust's type system includes a feature called deref coercion. This allows references to types that implement Deref to be treated as references to the target type. String implements Deref to str. This means &String can become &str automatically.

This coercion happens in function calls, variable assignments, and method resolution. It is why you can pass a &String to a function expecting &str without writing as_str().

fn process_text(text: &str) {
    // Function accepts a view.
    // It does not take ownership.
    println!("Processing: {}", text);
}

fn main() {
    let owned = String::from("Data to process");

    // &owned is &String.
    // Deref coercion converts it to &str.
    process_text(&owned);

    // You can also pass a literal directly.
    // Literals are &str, so no coercion is needed.
    process_text("Direct text");
}

This coercion is one-way. You cannot coerce &str to &String. That would imply the &str owns a String, which is false. If you need a String, you must allocate.

Deref coercion saves you from writing boilerplate. Trust the compiler to handle the conversion when you pass a reference.

Realistic pattern: Flexible functions

In real code, you often want functions that accept both string literals and owned strings. The solution is to accept &str. This lets callers pass literals, borrowed strings, or references to owned strings. The function reads the data without caring where it came from.

If the function needs to store the data, it converts to String internally. This keeps the API flexible while ensuring the struct owns its data.

struct User {
    // User must own the name.
    // The name survives after the function returns.
    name: String,
}

impl User {
    // Accept &str to allow literals and borrows.
    // Convert to String to take ownership.
    fn new(name: &str) -> Self {
        User {
            // Allocation happens here.
            name: name.to_string(),
        }
    }

    // Accept &str for comparison.
    // No allocation needed.
    fn has_name(&self, target: &str) -> bool {
        // Compare slices directly.
        // String implements PartialEq<&str>.
        self.name == target
    }
}

fn main() {
    // Pass a literal.
    let user1 = User::new("Alice");

    // Pass a borrowed String.
    let name_buf = String::from("Bob");
    let user2 = User::new(&name_buf);

    // Check names without allocation.
    println!("Match: {}", user1.has_name("Alice"));
}

This pattern is everywhere in the standard library. Functions like println! and format! accept &str arguments. Methods on collections often take &str for keys. Design your APIs to accept &str unless you must mutate the input.

Accept &str in your functions. Convert to String only when you need to store the data.

Pitfalls and compiler errors

The most common mistake is trying to return a &str from a function that points to a local String. The String is dropped at the end of the function. The &str would point to freed memory. Rust blocks this at compile time.

// This code does not compile.
// fn bad_function() -> &str {
//     let s = String::from("Local data");
//     &s // Error: s does not live long enough
// }

The compiler rejects this with E0515 (cannot return value referencing local variable). The error message tells you exactly what's wrong. The local variable s is dropped, so the reference would be dangling.

The fix is to return a String. This transfers ownership to the caller. The caller is responsible for freeing the memory.

fn good_function() -> String {
    let s = String::from("Owned data");
    // Return the String.
    // Ownership moves to the caller.
    s
}

Another pitfall is storing a &str in a struct without a lifetime. If the struct holds a reference, the struct cannot outlive the data it points to. This adds complexity to your API. If you are unsure, store a String. It is simpler and safer.

struct Cache {
    // Cache owns the keys.
    // No lifetime parameters needed.
    keys: Vec<String>,
}

impl Cache {
    fn add(&mut self, key: &str) {
        // Convert to String to store.
        self.keys.push(key.to_string());
    }
}

Lifetimes are powerful, but they add cognitive load. Use String in structs unless you have a measured reason to avoid allocation.

Decision matrix

Use &str for function parameters when the function only reads the text and does not need to store it. Use String for struct fields when the struct must own the data and keep it alive across calls. Use &str for string literals in code; they live in the binary and never need allocation. Use String when you need to modify the text, append characters, or build a result dynamically. Use to_string() when you have a &str and need to take ownership of the bytes. Use as_str() only when you need to explicitly document that you are extracting a slice, though implicit coercion usually handles this. Use String::from() if you prefer type-first syntax, but prefer to_string() for readability as it matches the community convention. Use &String only when you need to pass ownership of a reference, which is rare; &str is almost always better.

Pick &str for flexibility. Pick String for ownership. The compiler will guide you if you get it wrong.

Where to go next