Start learning free

LESSON 65 OF 414 · STRINGS I

String vs &str in Rust: Owned and Borrowed Text

Lesson Read the full explanation

Most languages have one string type. Rust has two, and telling them apart is the single biggest string hurdle for beginners:

  • &str — a borrowed slice: a read-only view into text that lives somewhere else. Every string literal like "Algo" is a &str. You can read it, but you can never grow or shrink it.
  • String — an owned, growable buffer. Your program owns the bytes, so it may append to them, clear them, or hand them to someone else.

Moving between the two is everyday Rust: call .to_string() on a slice to get your own String, and borrow a &str back out of a String whenever you only need to look:

let mut name: String = "Al".to_string(); // owned copy of the literal
name.push_str("go");                     // grow it — only String can do this
let peek: &str = &name[0..2];            // borrow a read-only window: "Al"

&name[0..2] is a range slice: it borrows the bytes from index 0 up to (but not including) 2. And a plain &String automatically coerces to &str, which is why functions usually accept &str.


The trap:

Sooner or later you will hand a literal to something that wants a String. The compiler stops you with this exact error — read the help: line, it hands you the fix:

error[E0308]: mismatched types
  |
2 |     let owned: String = "Algo";
  |                ------   ^^^^^^ expected `String`, found `&str`
  |                |
  |                expected due to this
  |
help: try using a conversion method
  |
2 |     let owned: String = "Algo".to_string();
  |                               ++++++++++++

The literal is a borrowed &str; the annotation demands an owned String. They are different types, and the conversion — .to_string() — is always explicit. The reverse mistake is trying to grow a literal: "Algo".push_str("Cademy") fails too, because push_str only exists on String.


Assignment
Follow the Coding Tutorial to make the round trip: borrowed → owned → borrowed again.

  • Bind the literal "Algo" to slice with the explicit type &str.
  • Convert it into a mutable owned String named owned using .to_string().
  • Append "Cademy" with push_str and print owned.
  • Slice the first four bytes back out as first_four: &str using &owned[0..4] and print it.
  • Print the length of owned with .len().

The exact expected output is:

owned = AlgoCademy
first_four = Algo
len = 10

Hint
If the compiler says expected `String`, found `&str`, you forgot .to_string(). If push_str complains, check two things: the target must be a String (not a literal) and its binding must be mut.


STEP 1 OF 7
    Ctrl + Enter to check

    RUST · STRINGS I

    String Vs Str in Rust, explained

    Two string types

    Most languages have one string type. Rust has two that you will meet in your first program, and the difference explains many early compiler errors.

    • &str (a "string slice") is a borrowed, read-only view of text stored somewhere else. Every string literal such as "Algo" is a &str. You can read it, slice it and print it, but you can't append to it.
    • String is an owned, growable buffer. Your code owns the bytes, so it may append to them, clear them, or hand them to another function.

    Think of &str as looking at a page in a book and String as having your own notebook that you can write in.

    Converting between them

    From borrowed to owned, call to_string() on the slice. It copies the text into a new buffer. String::from("Algo"), "Algo".to_owned() and "Algo".into() (when the type is known) do the same:

    let mut name: String = "Al".to_string();
    name.push_str("go");
    let peek: &str = &name[0..2];
    println!("{} {}", name, peek);
    
    Algo Al
    

    From owned back to borrowed, put a & in front. A range slice such as &name[0..2] borrows bytes 0 and 1. name.as_str() borrows the whole thing. A &String also converts to &str automatically where a &str is expected, which is why functions normally take &str parameters: they accept literals and borrowed Strings alike.

    fn shout(s: &str) -> String {
        s.to_uppercase()
    }
    

    Calling shout("lit") and shout(&owned) both work. A function that takes a String parameter, on the other hand, takes ownership of the value, so callers have to pass an owned string.

    The classic errors

    Three mistakes account for most trouble, and each has a clear compiler message.

    1. A literal where a String is required. let owned: String = "Algo"; fails with expected `String`, found `&str`. The compiler's help: line suggests the fix, "Algo".to_string(). The conversion is never implicit, because it allocates memory and Rust keeps that visible.
    2. Growing a &str. "Algo".push_str("Cademy") fails with no method named `push_str` found for reference `&str`. The method exists only on String.
    3. Forgetting mut. Even on a String, push_str needs a mutable binding. Without it the compiler says cannot borrow `owned` as mutable, as it is not declared as mutable.

    Making a &str variable mut doesn't change its type. It lets the variable point at a different slice, but the text it views still can't grow.

    The lesson's exercise

    The exercise makes the round trip from borrowed to owned and back to borrowed. The steps are:

    1. Bind the literal to slice with the explicit type &str.
    2. Convert it into a mutable String named owned with to_string(). It is mut because the next step changes it.
    3. Append "Cademy" with push_str, which works because owned is a String.
    4. Print it with the label owned = in front.
    5. Borrow a view back into first_four with &owned[0..4]. The & matters, because owned[0..4] by itself is a bare str of unknown size and can't be stored in a variable.
    6. Print first_four with its label. A &str prints with {} just like a String.
    7. Print the length of owned, which is 10.
    let slice: &str = "Algo";
    let mut owned: String = slice.to_string();
    owned.push_str("Cademy");
    println!("owned = {}", owned);
    let first_four: &str = &owned[0..4];
    println!("first_four = {}", first_four);
    println!("len = {}", owned.len());
    
    owned = AlgoCademy
    first_four = Algo
    len = 10
    

    Three Rust details show up here. First, len() counts bytes, which equals the number of characters for ASCII text like this. Second, while first_four is alive it borrows owned, so Rust would reject a change to owned that comes before the last use of first_four. Third, slicing copies nothing: first_four points into the same buffer.

    Key takeaways

    • A literal is a &str: borrowed, read-only, fixed.
    • A String is owned and growable, and it needs mut to be changed.
    • .to_string() turns a slice into a String; & or as_str() borrows a &str back.
    • Prefer &str for function parameters; the compiler's help: lines usually show the conversion you need.
    Keep going with RustAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free