---
title: "9.7 String slices"
description: "&str fully explained at last: string slices, range syntax on text, why parameters take &str over &String, and deref coercion."
url: "https://learnrust.net/chapter-9/string-slices/"
last_updated: "2026-06-12"
---

# 9.7 String slices

In lesson [5.4](https://learnrust.net/chapter-5/introduction-to-str/), you were told to pronounce `&str` "string slice" and to take the name on faith. Four chapters later, the faith pays out: a `&str` is exactly last lesson's machinery applied to text. It's a slice whose elements are the bytes of a string: an address, a length, and no ownership. Every fact you know about it (it views, it can't grow, it's cheap, literals are one) falls out of that.

The carving syntax works on Strings precisely like it worked on arrays:

```rust
fn main() {
    let s = String::from("hello world");

    let hello = &s[..5];
    let world = &s[6..];

    println!("{hello} | {world}");
}
```

```
hello | world
```

`hello` and `world` are `&str`s, windows onto byte ranges of text that `s` owns on the heap. No text was copied; two handles were made. And the indexes count *bytes*, not characters, a distinction lesson [5.3](https://learnrust.net/chapter-5/introduction-to-string/)'s "café" made you respect; we'll meet its consequences at the end of the lesson.

## Literals, and the spelling from lesson 4.7

A string literal's text is baked into your compiled program (lesson 5.4), so `let motto = "fearless";` makes `motto` a slice into the program binary itself: a view of text that's alive from the first instruction to the last. Rust has a name for that longest-possible lifetime, and you've seen it: lesson [4.7](https://learnrust.net/chapter-4/introduction-to-if-expressions/)'s quiz had you write `&'static str` on faith, and last lesson's E0106 transcript offered `'static` as the compiler's first (unlikely) suggestion. The `'static` is a lifetime name, the only one you'll see written out until chapter 17, and it means "lives for the entire program run." A view of the binary can promise that; a view of a local String can't, which is why the compiler called the suggestion uncommon.

## The function that needs a slice

Here's the problem string slices were born for. Write a function that finds the first word in some text. Without slices, the best you can return is *where the word ends*:

```rust
fn first_word_end(s: &String) -> usize {
    let bytes = s.as_bytes();
    let mut i = 0;
    while i < bytes.len() {
        if bytes[i] == b' ' {
            return i;
        }
        i += 1;
    }
    s.len()
}
```

Two small novelties, both honest previews. `s.as_bytes()` lends the String's raw bytes as a `&[u8]`, last lesson's slice type with lesson [4.2](https://learnrust.net/chapter-4/integer-types/)'s smallest integer. And `b' '` is a **byte literal**: the space character as its byte value (a `u8`), so it can be compared against the slice's elements. The `while`-and-index scan is lesson 9.6's pattern. We walk the bytes; at the first space we return its position; no space means the whole string is one word, so we return its length.

It compiles, it returns 5 for `"hello world"`, and it has a bug you can't see at the return site. The `usize` it returns is only *about* the String it scanned; nothing ties them together. Watch the answer outlive its question:

```rust
fn main() {
    let mut s = String::from("hello world");
    let end = first_word_end(&s);
    s.clear();
    println!("the first word ends at byte {end}");
}
```

```
the first word ends at byte 5
```

`clear` is new but unsurprising: it empties the String (a writable-loan method, like `push_str`). The program compiles, runs, and confidently reports a fact about text that no longer exists. `end` says 5; `s` is empty; both are "right"; the combination is a lie. The C++ chapter this lesson adapts spends real estate teaching the discipline to manage exactly this kind of stale bookkeeping by hand.

## The slice version

Return a *view of the word* instead of a number about it, and the bookkeeping becomes the borrow checker's job:

```rust
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    let mut i = 0;
    while i < bytes.len() {
        if bytes[i] == b' ' {
            return &s[..i];
        }
        i += 1;
    }
    s
}
```

Same scan, but the answer is now `&s[..i]`, a slice of the input up to the space (or the whole input, `s`, if it's all one word). The return type is `&str`, the signature is lesson 9.5's safe shape (a view borrowed from a parameter), and the answer is now physically attached to the text it describes. Run the same betrayal as before:

```rust
fn main() {
    let mut s = String::from("hello world");
    let word = first_word(&s);
    s.clear();
    println!("the first word is: {word}");
}
```

```
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
  --> src/main.rs:16:5
   |
15 |     let word = first_word(&s);
   |                           -- immutable borrow occurs here
16 |     s.clear();
   |     ^^^^^^^^^ mutable borrow occurs here
17 |     println!("the first word is: {word}");
   |                                   ---- immutable borrow later used here
```

Patient 2 from lesson [9.3](https://learnrust.net/chapter-9/the-borrow-checker-is-your-friend/), recognized on sight by now. But pause on what just happened, because it's the chapter's thesis in one transcript. The index version had the same logical bug and *compiled*. The slice version makes the bug a type error, because the connection between answer and text, which used to live in a comment or the programmer's memory, now lives in the reference where the borrow checker can see it. Rust isn't just protecting memory here; the memory in the index version was never unsafe. It's protecting the *meaning* of your data. Reorder (print the word, then clear) and it compiles happily, printing `the first word is: hello`.

## Why `&str` beats `&String`, mechanically at last

Notice the parameter: `first_word(s: &str)`, not `&String`, even though `main` passes `&s` where `s` is a String. Lesson 5.4 told you this works and promised the mechanism in chapter 9. The mechanism is called **deref coercion**: where a `&str` is expected and a `&String` is supplied, the compiler automatically converts, by handing over a view of all the String's text. (Last lesson's array-to-slice conversion is a sibling rule with the same shape: owner-flavored reference in, view out.)

The arithmetic of the choice: a `&str` parameter accepts a `&String` (coerced), a literal (already a `&str`), or any slice of either (`&s[..5]`). A `&String` parameter accepts only the first. Same function body either way, strictly more callers served, which makes 5.4's best practice a closed case instead of a house rule.

> **Best practice**
>
> Take `&str` for read-only text parameters, always. The same logic now extends up a level: prefer `&[i32]` over `&[i32; 5]`, and in general, take the widest view type that can do the job.

## The byte-boundary fine print

One warning before the chapter closes, owed since "café". Slicing counts bytes, but a `char` can occupy up to four of them (lesson [4.8](https://learnrust.net/chapter-4/char-and-unicode/)), and Rust refuses to manufacture a view of *half a character*. Cut at a clean boundary and all is well; cut mid-character and the program panics. With the cut point from input (the compile-time-arithmetic lesson of 3.1 and 4.4 applies to text too):

```rust
use std::io;

fn main() {
    let order = String::from("café au lait");

    let mut end = String::new();
    io::stdin().read_line(&mut end).expect("failed to read line");
    let end: usize = end.trim().parse().expect("not a number");

    let prefix = &order[..end];
    println!("prefix: {prefix}");
}
```

Type `3` and you get `prefix: caf`. Type `4`, which lands between `é`'s two bytes:

```
thread 'main' (2304854) panicked at src/main.rs:10:24:
end byte index 4 is not a char boundary; it is inside 'é' (bytes 3..5 of string)
```

The panic message is a small masterpiece: which index, why it's illegal, which character it stabbed, and that character's true byte range. For the English-y examples of the next few chapters, byte positions you *found by scanning the text* (like `first_word`'s space positions) are always safe cut points; arbitrary numbers from elsewhere are not. The full toolkit for walking text character-by-character arrives with chapter 18's deeper String coverage.

## Quiz time

**Question #1**

Name the type of each variable, no compiler allowed:

```rust
fn main() {
    let motto = "fearless";
    let owned = String::from("fearless concurrency");
    let word = &owned[..8];
    let counts = [3, 1, 4];
    let pair = &counts[..2];
    println!("{motto} {owned} {word} {pair:?}");
}
```

<details class="solution">
<summary>Show solution</summary>

`motto` is a `&str` (a literal: a view into the binary, and the one variable here entitled to the full `&'static str` spelling). `owned` is a `String`. `word` is a `&str`, an 8-byte view into `owned`'s heap text. `counts` is an array, `[i32; 3]`. `pair` is a `&[i32]`, last lesson's slice. Two slice types, one concept, four total characters of difference in their spellings.

</details>

**Question #2**

Using this lesson's `first_word`, predict the compiler's reaction, then fix by reordering:

```rust
fn main() {
    let mut headline = String::from("rust ships");
    let lead = first_word(&headline);
    headline.push_str(" today");
    println!("{lead}");
}
```

<details class="solution">
<summary>Show solution</summary>

E0502: `lead` is a live read-only borrow of `headline` (it's a view into it, used at the `println!`), and `push_str` needs a writable loan in between. And it's the full horror scenario from lesson 9.2: growing `headline` could relocate its text, leaving `lead` pointing at a freed block, so this refusal is load-bearing. Fix: print first, push after.

```rust
fn main() {
    let mut headline = String::from("rust ships");
    let lead = first_word(&headline);
    println!("{lead}");
    headline.push_str(" today");
}
```

This prints `rust`.

</details>

**Question #3**

Write `fn last_word(s: &str) -> &str`, returning everything after the final space (or the whole string if there's no space). Use `first_word`'s scanning pattern.

<details class="solution">
<summary>Show solution</summary>

```rust
fn last_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    let mut start = 0;
    let mut i = 0;
    while i < bytes.len() {
        if bytes[i] == b' ' {
            start = i + 1;
        }
        i += 1;
    }
    &s[start..]
}
```

Instead of returning at the first space, keep scanning and remember the position just past the *latest* space seen; the answer is the slice from there to the end. With no spaces, `start` stays 0 and the whole string comes back, matching `first_word`'s behavior for one-word input. (`last_word("hello world")` is `"world"`; and the returned view borrows from `s`, so the borrow checker guards it exactly like `first_word`'s.)

</details>

That's the chapter's full toolkit: two kinds of loans, two rules, four famous refusals, and views you can aim at any part of a sequence. The summary lesson collects it all in one place and then makes you use every piece at once.

## Sitemap

See the full [sitemap](https://learnrust.net/sitemap.md) for all pages.
