The Slice Type

The type that turns a whole class of "the data changed underneath me" bugs into compile errors: string slices, range syntax, and why &str shows up absolutely everywhere in idiomatic Rust.

11 min read4 learning objectives

What You'll Learn

  • Recognize the "stale index" bug that slices exist to prevent
  • Read and write string slices using range syntax
  • Rewrite an index-returning function to return a slice — and watch the compiler enforce its correctness for you
  • Generalize from string slices to slices of any collection

Right where we left off: imagine you wrote a function that returns a usize — a byte index marking where the first word in a string ends. It compiles. It works in your test. And it's quietly hiding a bug that won't show up in code review, in your test suite, or in staging. It'll show up in production, on a Tuesday, in a way that takes an afternoon to track down. Let's look at exactly why — and then meet the type that makes this bug impossible to write in the first place.

A number that quietly goes stale

Here's that function, doing exactly what it claims to do:

Walk through what just happened. word holds 5 — genuinely the correct answer, at the moment first_word returned it. Then s.clear() truncates the string back to empty, and... word is still 5. Nothing updated it, because nothing couldword is just a number, with zero ongoing connection to the string it was supposedly describing. The value and the data it once described have quietly drifted apart, and the compiler has no way to warn you, because as far as the type system can tell, you're simply holding a usize. Like any other one.

This bug has names in other languages — "stale index," "iterator invalidation," pick your favorite war story. Tracking it down usually means a debugger session and a long sigh. Rust's fix for it isn't a convention, a linter rule, or a comment that says "be careful here." It's a type.

String slices: a reference to part of a string

A string slice is a reference to a contiguous chunk of a String — not a copy, a genuine view into the original data, written as a range inside square brackets:

Internally, a slice is just a pointer to a starting byte plus a length — but that's an implementation detail you rarely need to think about day to day. What matters is the type: &s[0..5] has type &str, and — this is the important part — it stays tethered to s for as long as it exists. This is the borrow checker again, just wearing a different hat.

Ranges come with shortcuts

Typing s.len() as an endpoint constantly would get old fast, so Rust's range syntax has two shorthands worth knowing immediately:

Use whichever form makes the intent clearest at the call site. &s[..] in particular is one you'll see constantly — it means "all of it, as a slice," and it shows up whenever a function wants &str but what you're holding is a String.

Rewriting first_word the right way

Now let's fix the function — same logic, one change to the return type:

One word changed in the signature — usize became &str — and the body barely moved: instead of handing back the raw index i, we return &s[0..i], an honest-to-goodness slice of the original string. The return value is no longer a number that might describe s. As far as the type system is concerned, it is part of s — on the record, permanently, for as long as it lives.

Now watch the compiler refuse to let the bug happen

Let's run the exact same "clear it out from under me" scenario from the top of this lesson — but with the new, slice-returning version:

There it is — the exact bug from the start of this lesson, except now it's a compile error instead of a silent data corruption waiting to happen. first_word(&s) creates an immutable borrow that word depends on for as long as it's alive; s.clear() needs a mutable borrow to do its job; and you already know from the last lesson that those two cannot coexist. The type system isn't merely describing the relationship between word and s anymore — it's enforcing it, automatically, for free, on every single build, for the rest of this program's life.

Wait — &str looks familiar...

If that return type rang a bell, good catch — you've been looking at it since lesson one. Every string literal in your code has been a slice the entire time:

"Hello, world!" is baked directly into your compiled binary, and s is a reference pointing at that exact spot in memory — which is precisely why string literals are immutable: &str is a reference, and you cannot mutate through a plain reference, full stop. It's also why function parameters that only need to read string data should usually be typed &str rather than &String — a &str parameter happily accepts a literal, a full String reference, or a slice of one, making it the most flexible choice for the least commitment. (We'll come back to this exact piece of advice once we reach strings in depth.)

Slices aren't just for strings

The exact same idea generalizes to any contiguous sequence in memory. Here it is on a plain array:

&a[1..3] has the type &[i32] — a slice of i32s — and it behaves exactly like a string slice: a pointer plus a length, borrowed from the original array, kept honest by the very same borrow-checker rules you already know. You'll see this exact pattern constantly once we get to vectors.

Two ideas worth holding onto

  • A slice is a reference to part of a collection — not a copy, a borrowed view, carrying the exact same compile-time guarantees as any other reference.
  • &str ("string slice") is the type to reach for whenever a function needs to read string data without caring whether the caller has a String, a literal, or a slice of something bigger. In idiomatic Rust, you'll write &str far more often than String itself.

Quick exercise

Take the broken main from the "compiler catches it" example above, and fix it so it compiles and prints the word correctly — there's more than one valid way to do this, so see if you can find two. Then deliberately break it again, a different way, and actually read the new error Rust shows you. The more of these you read voluntarily and on purpose, the faster they stop feeling like punishment and start feeling like a second pair of eyes on your code.

With that, you've now covered the three lessons that — more than anything else — separate Rust from every language you've used before: ownership, borrowing, and slices. Everything from here builds on a foundation that simply does not allow whole categories of bugs to exist. Time to start building with that foundation: structs, Rust's way of bundling related data into types of your own design.