What is the difference between String and str

String owns heap-allocated text and can grow. &str is a borrowed view into UTF-8 bytes someone else owns. Learn the memory layout, when to use each, and why function arguments almost always take &str.

Two strings? Why?

When you come from Python or JavaScript, strings feel like a solved problem. There's the string type, you make one, you concatenate, you slice, life is good. Then you write your first Rust function:

fn shout(name: &str) -> String {
    format!("{}!", name.to_uppercase())
}

And suddenly there are two of them, with different syntax, and one has an ampersand in front. Why does Rust make this so weird?

The answer is that what Python's str (or JS's string) hides from you, Rust has to expose. There are really two questions about any piece of text: who owns the memory, and where does the memory live? Rust gives those two facts to two different types.

  • String is an owned, growable, heap-allocated buffer of UTF-8 bytes. It's what you'd build at runtime out of pieces.
  • str (almost always seen as &str) is a borrowed view into a piece of UTF-8 text that lives somewhere else. It doesn't own anything. It's a pointer plus a length.

Once you internalize that split, the rest of Rust's string story becomes pretty natural.

What each one looks like in memory

A String is essentially the same shape as a Vec<u8>. It's three words on the stack: a pointer to a heap allocation, a length, and a capacity. The actual UTF-8 bytes live on the heap. As you push more characters, the heap allocation may grow.

stack:        heap:
+--------+    +-----------------+
| ptr ---|--->| h | e | l | l | o |
| len 5  |    +-----------------+
| cap 8  |
+--------+

A &str is a fat pointer: just two words, a pointer and a length. It doesn't have a capacity because it doesn't own anything: it's a window into bytes that someone else is keeping alive. The bytes might be on the heap, on the stack, or in your program's read-only data section (that's where string literals live).

stack (your &str):
+--------+
| ptr ---|---> ... bytes somewhere ...
| len 5  |
+--------+

This is why &str shows up as str plus a reference: str itself is unsized (the compiler doesn't know how many bytes there are at compile time), so you can never hold a bare str value. You always work with it through a reference, which adds the length.

What about string literals?

When you write let x = "hello"; in Rust, the literal "hello" is a &'static str. The bytes are baked into the executable's read-only data section. The variable x holds a fat pointer (ptr + len) into that section. No allocation happens, no copying. It's just a pointer with a fixed-for-the-program-lifetime length.

The 'static lifetime means "these bytes live as long as the program does." That's why you can return string literals from functions without lifetime headaches: they're never going away.

The basic conversions

Getting between the two is constant in Rust code, so it pays to memorize the moves:

fn main() {
    // String literal -> &str. No work, the literal already is a &str.
    let s: &str = "hello";

    // &str -> String. Allocates and copies the bytes into a fresh heap buffer.
    let owned: String = s.to_string();
    let owned2: String = String::from(s);
    let owned3: String = s.to_owned();   // for trait users

    // String -> &str. Free: deref coercion gives you a borrow into its buffer.
    let borrow: &str = &owned;
    let borrow2: &str = owned.as_str();

    // Build a String from pieces.
    let mut buf = String::new();
    buf.push_str("hello, ");
    buf.push_str("world");
    buf.push('!');
    println!("{}", buf);
}

The ones that allocate (to_string, from, to_owned) are doing real work: they copy bytes onto the heap. The ones that borrow (as_str, &my_string) are nearly free.

A subtle point: &owned triggers "deref coercion." String implements Deref<Target = str>, so wherever a &str is expected, you can pass a &String and the compiler inserts the conversion. This is why shout(&owned) works even though shout takes &str.

Why function arguments almost always take &str

There's a strong convention in Rust: if your function only needs to read the string, take &str, not String or &String.

// Good. Accepts a borrow into anything UTF-8.
fn count_words(text: &str) -> usize {
    text.split_whitespace().count()
}

// Less good. Forces every caller to give up ownership of a String.
fn count_words_owned(text: String) -> usize {
    text.split_whitespace().count()
}

The &str version works with literals (count_words("hello world")), with borrowed &String (count_words(&my_string)), and with substrings (count_words(&full[5..])). The String version forces the caller to either clone or hand over their string. That's wasteful for the 99% case where you're just reading.

Only take String when the function genuinely needs to own the string: storing it in a struct, modifying it and returning it, sending it across threads. And even then, taking impl Into<String> is often friendlier.

A more realistic example

Suppose you're building a tiny config parser. You read a String from a file, split it into lines, and look for specific keys. The String is the owner. Each line you operate on is a &str that points into the original buffer.

fn find_value<'a>(text: &'a str, key: &str) -> Option<&'a str> {
    // Each line is a &str slice into the original text. No allocation.
    for line in text.lines() {
        // split_once returns Option<(&str, &str)>, again with no copying.
        if let Some((k, v)) = line.split_once('=') {
            // trim() also returns a &str, just nudging the start/end pointers.
            if k.trim() == key {
                return Some(v.trim());
            }
        }
    }
    None
}

fn main() {
    // The config is owned here on the stack (well, the heap, via String).
    let config = String::from("\
user = alice
port = 8080
host = localhost");

    // The returned &str borrows from `config`. As long as `config` lives,
    // the returned &str is valid. The lifetime annotation 'a says exactly that.
    if let Some(v) = find_value(&config, "port") {
        println!("port is {v}");   // prints: port is 8080
    }
}

The lifetime 'a ties the returned &str to the input &str. The compiler will refuse to let you keep using the returned slice after config is dropped. That's the whole point of borrowed strings: zero-copy slicing, with safety enforced at compile time.

Common pitfalls

You wrote fn greet(name: String) and a caller passed "alice". You'll see:

error[E0308]: mismatched types
  expected `String`, found `&str`

The fix is almost always: take &str instead. Or, if you need ownership, callers can write "alice".to_string().

You tried to index into a string by integer: s[3]. Compiler refuses, because UTF-8 characters aren't all one byte and s[3] would be ambiguous. Use s.chars().nth(3) for the 4th char, or slice by byte ranges (&s[3..5]) only when you know the boundary lands on a UTF-8 boundary.

You wrote let s: str = "hi"; and got:

error[E0277]: the size for values of type `str` cannot be known at compilation time

str is unsized; you can't have a bare str on the stack. Use &str.

You tried to return a &str from a function that builds a string locally:

fn bad() -> &'static str {
    let s = String::from("hello");
    &s  // ERROR: returning reference to local data
}

The String is dropped at end of function; any borrow into it is dangling. Return String instead, or compute the string from a literal that already has 'static lifetime.

You used == between a String and a &str and got confused. That actually works: String implements PartialEq<&str>. So my_string == "hello" is fine.

When to use each

Use String when you need to own the bytes: building a string at runtime, storing one in a struct that outlives the function, modifying it.

Use &str for everything else: function parameters, slicing, view types. It's lighter, more flexible, and works with everything (literals, Strings, substrings of strings).

Use Cow<'_, str> when you sometimes need to own and sometimes can borrow. For instance, a function that returns the input unchanged most of the time but occasionally has to allocate to fix something up.

Use Box<str> (rarely) when you have an immutable owned string and want one less word of overhead than String (no capacity field, since it can't grow).

Use Rc<str> or Arc<str> when many parts of your program need to share the same immutable string.

The rule of thumb: take &str, return String (when you allocate), and only depart from that when you have a specific reason.

Where to go next

How to convert between String and str

What is the Cow str type

How to iterate over characters