5.4Introduction to &str

Last updated June 12, 2026

Time for the reveal this chapter has been circling. Every string literal you've written since your first program (that's "Hello, world!" itself) was never a String. Ask the compiler, via a deliberately wrong annotation, and it'll name the real type:

fn main() {
    let greeting: i32 = "Hello, world!";
    println!("{greeting}");
}
error[E0308]: mismatched types
 --> src/main.rs:2:25
  |
2 |     let greeting: i32 = "Hello, world!";
  |                   ---   ^^^^^^^^^^^^^^^ expected `i32`, found `&str`
  |                   |
  |                   expected due to this

There it is, the type that's been photobombing error messages since lesson 4.9: &str (pronounce it "string slice"; out loud, Rust folks often just say "stir"). A &str is a view of text that lives somewhere else. It doesn't own the text, can't grow it, can't change it; it just looks at it, and knows where it starts and how long it is.

Owners and viewers

The cleanest way to hold both string types in your head at once is an analogy learncpp uses for the C++ equivalents, and it ports perfectly. Owning a bicycle is a commitment: you paid for it, you store it, you maintain it, and when you're done with it, disposing of it is your problem. Watching a bicycle through your window costs nothing: no purchase, no shed, no responsibility, and any number of neighbors can watch the same bicycle at once. But the watching only makes sense while the bicycle is actually there.

String is the owner: it has its own copy of the text and the storage that holds it (last lesson's "this one's mine"). &str is the viewer at the window: a cheap way to refer to text that something else keeps alive. A string literal's text is baked directly into your compiled program, so a literal like "hello" is a view of text that's there for the program's entire run, which is why a &str variable can use it forever without anyone owning anything.

You've been using views all chapter

Now the trim mystery from lessons 1.6 and 5.2 dissolves. trim takes your String and returns... what, exactly? It doesn't build a cleaned-up copy of the text. It returns a &str: a view into the same text the String owns, with the start nudged past the leading whitespace and the length shortened to skip the trailing newline. Nothing is copied. Nothing is allocated. The idiom

let name = name.trim();

is a String being shadowed by a view into itself, which is why it's so cheap, and why 5.2 could only wink at the type. (It's also your first hint of a deep Rust habit: when an operation can be done by viewing instead of copying, Rust's standard library almost always does, and tells you so in the return type.)

&str as the parameter type

The practical payoff of views is function signatures. Suppose you're writing a greeter:

fn greet(name: &str) {
    println!("Welcome, {name}!");
}

fn main() {
    greet("Ada");

    let typed = String::from("Grace");
    greet(&typed);
}
Welcome, Ada!
Welcome, Grace!

One function, both flavors welcome. A literal is already a &str, so it walks right in. For the String, writing &typed lends the function a view of the owned text (that & is the same "lend, don't hand over" marker you've been writing in read_line(&mut name) since lesson 1.6, minus the permission to modify; chapter 9 is entirely about this machinery, and lesson 9.2 settles the tab). Forget the & and the compiler draws the type boundary for you:

fn greet(name: &str) {
    println!("Welcome, {name}!");
}

fn main() {
    let typed = String::from("Grace");
    greet(typed);
}
error[E0308]: mismatched types
 --> src/main.rs:7:11
  |
7 |     greet(typed);
  |     ----- ^^^^^ expected `&str`, found `String`
  |     |
  |     arguments to this function are incorrect
  |
note: function defined here
 --> src/main.rs:1:4
  |
1 | fn greet(name: &str) {
  |    ^^^^^ ----------
help: consider borrowing here
  |
7 |     greet(&typed);
  |           +

The function wants a view; you offered the whole bicycle; the compiler suggests the window.

Best practice

Store text in a String; accept text parameters as &str. An &str parameter takes literals and (with &) Strings alike, costs nothing to pass, and promises the caller you'll only look.

Key insight

If C++ is your previous language, the viewer pattern should be raising an alarm: what happens when a view outlives the text it watches? std::string_view makes that a famous family of bugs (the bicycle is gone; the neighbor is still staring at the empty driveway). In Rust, the question has a different answer: programs where that can happen do not compile. How the compiler proves it can't happen is the borrow checker, the star of chapter 9 and the single most distinctive thing about Rust. Consider this chapter the calm before that storm.

What this lesson deliberately skips: making a view of part of a string (the first word, say). Carving views by range is real and routine, but it rides on machinery from chapters 9 and 18, so it waits. Two string types, one relationship (owner, viewer) is the entire syllabus for today.

Quiz time

Question #1

Name the type of each variable, no compiler allowed:

fn main() {
    let a = "library";
    let b = String::from("library");
    let c = b.trim();
    let d = c.len();
    println!("{a} {b} {c} {d}");
}
Show solution

a is a &str (literals always are). b is a String (it says so). c is a &str: trim returns a view into b's text (here an unmoved one, since there's no whitespace to skip). d is a usize, lesson 4.3's memory-measurement type, counting 7 bytes.

Question #2

For each function, pick the parameter type, String or &str, and say why:

a) fn shout(message: ???) — prints the message followed by three exclamation marks b) fn label_width(label: ???) — returns the label's len() for a layout calculation

Show solution

Both are &str, and for the same reason: each function only needs to look at the text. The best-practice rule from this lesson decides every case in this chapter (and most cases after it): parameters that read text take &str. Cases that justify a String parameter exist, but they involve the function keeping the text, which is ownership business for chapter 8.

Question #3

Predict the compiler's reaction, then give the one-character fix:

fn cheer(team: &str) {
    println!("Go {team}!");
}

fn main() {
    let team = String::from("Ferris");
    cheer(team);
}
Show solution

error[E0308]: mismatched types at the call: expected &str, found String, with the compiler's own help line offering the fix: consider borrowing here, i.e. cheer(&team);. One &, and the function gets its view.

Both string types are now on the table, which means the chapter can pay its biggest pile of IOUs: the formatting machinery that turns values into Strings, owed since lesson 2.4 and teased in four separate chapter 4 lessons. Next.