Recommended resources
The freely available resources this lesson will direct you to.
Hello! In the previous lesson, you lent a function either read access with & or exclusive write access with &mut, while keeping ownership with the caller. A slice applies the same borrowing idea to a portion of a value. This lesson covers slices of arrays and UTF-8 strings: how to create them, what their indices mean, and why the borrow checker keeps a view valid while you use it. These are useful when a backend function needs to inspect part of a request or a sequence of values without making a copy.
A view, not a new collection
An array has a known number of elements in its type: [i32; 5] is an array of exactly five i32 values. An array slice, written &[i32], is a borrowed view of some consecutive i32 values. Its length can vary from one call to another.
Watch the selected parts of Rust: Slice Types by The Dev Method for a visual introduction. The final short segment applies the same idea to arrays; you can leave its discussion of function-interface choices for the next lesson.
Watch Rust: Slice Types by The Dev Method to see a range become a borrowed view, then see why that view remains connected to its owner.
Watch string ranges for the basic slice syntax. Continue with borrowed results, focusing on why changing the original string can conflict with a slice still in use. Skip ahead to array slices to see that the idea is not specific to text.
Here is an array and a view of three of its elements:
fn main() {
let amounts: [i32; 5] = [10, 20, 30, 40, 50];
let middle: &[i32] = &amounts[1..4];
println!("{middle:?}"); // [20, 30, 40]
println!("{}", middle[0]); // 20
println!("{amounts:?}"); // The owner still has the array.
}
The range 1..4 includes index 1 and excludes index 4. Importantly, middle[0] is amounts[1]: indices into a slice start at zero relative to the beginning of that slice.
The diagram below shows two slices of one array. Each view records where its portion starts and how long it is; neither owns a separate copy of the elements.

This distinction matters even when the elements happen to be cheap to copy. Creating a slice does not copy the selected elements. An immutable slice lends read access to them for as long as the borrow is needed. Like other indexed access, an invalid range or an out-of-bounds slice index causes a runtime panic rather than allowing access to unrelated memory.
You can omit a range endpoint when you want the start or end of the collection:
let values = [10, 20, 30, 40, 50];
let first_two = &values[..2]; // [10, 20]
let from_two = &values[2..]; // [30, 40, 50]
let all = &values[..]; // [10, 20, 30, 40, 50]
Notice that &values borrows the array and has type &[i32; 5], whereas &values[..] explicitly creates a slice with type &[i32]. Both borrow rather than take ownership; the latter describes a view whose length is not fixed in its type.
A string slice views UTF-8 text
The same range notation works with text, but the types differ. A String owns growable UTF-8 data. A &str is a borrowed view of valid UTF-8 data. A string literal such as "queued" is also a &str; it refers to text stored with the program rather than borrowing from a String you created.
fn main() {
let status = String::from("queued for review");
let first: &str = &status[..6];
let remainder: &str = &status[7..];
println!("{first}"); // queued
println!("{remainder}"); // for review
println!("{status}"); // queued for review
}
The ranges describe portions of the original data. They do not create two new owned strings, and status remains the owner.
Read the official Rust book’s The Slice Type for the connection between range syntax, borrowed results, and the borrowing rule from last lesson. Its “Other Slices” subsection then relates string slices back to arrays.
The Slice Type - The Rust Programming Language
Read The Slice Type in the official Rust book to see why returning a slice is safer than returning an index into data that may later change.
In “String Slices,” read range notation, starting where the book contrasts a portion with the entire string and ending at its character-boundary warning. Stay in “String Slices” and read the first-word example, through the explanation of the conflicting clear call. Finally, in “Other Slices,” read the array example. Focus on what stays owned and what is merely borrowed.
A slice can also be the result of a function. The function below finds the first ordinary space and returns a view of the text before it:
fn before_space(text: &str) -> &str {
for (byte_index, character) in text.char_indices() {
if character == ' ' {
return &text[..byte_index];
}
}
text
}
fn main() {
let request = String::from("café au lait");
let first = before_space(&request);
println!("{first}"); // café
println!("{request}"); // café au lait
}
char_indices() supplies each character together with its starting byte index. That makes the range endpoint suitable for slicing. If there is no space, returning text gives the caller a view of the whole input. In either case, the return value borrows from the input; the function has not manufactured an independent string. A borrowed view cannot remain usable after the data it views has gone away.
This connection also explains a useful compiler error:
let mut request = String::from("queued for review");
let first = &request[..6];
request.clear(); // Does not compile: first is still needed.
println!("{first}");
clear would change the String while first still refers into it. The immutable slice’s later use keeps its borrow active, so Rust rejects the conflicting mutable access. If you print first before calling clear and never use it again, the borrow can end before the change.
String ranges count bytes, not visible characters
Array range indices count elements. String range indices count UTF-8 bytes. These happen to line up for the letters in "queued", but not for every language or symbol:
fn main() {
let word = String::from("café");
println!("{}", word.len()); // 5 bytes
println!("{}", &word[0..3]); // caf
println!("{}", &word[3..5]); // é
}
The é occupies two bytes in UTF-8. A range such as &word[0..4] ends inside that character and panics at runtime. Rust also does not allow word[3] to retrieve a single character: a byte position does not reliably identify a character.
The official book’s Storing UTF-8 Encoded Text with Strings develops the reason for this restriction and shows the string-slicing boundary error. Read these portions rather than treating the byte counts as an arbitrary Rust convention.
Storing UTF-8 Encoded Text with Strings
Read the official Rust book’s discussion of UTF-8 representation and string slicing. It explains why an apparently plausible numeric index may not describe a valid piece of text.
In “Indexing into Strings,” start at the indexing explanation and read through the “Internal Representation” subsection to its conclusion. Then go to “Slicing Strings”: read the slicing discussion, ending after the invalid-boundary example. Focus on the difference between a byte offset and a valid character boundary.
This does not mean that slicing non-ASCII text is forbidden. It means both endpoints must be valid boundaries. In before_space, char_indices() provides valid starting positions, so its byte offsets can be used for ranges. If you need to inspect characters rather than cut out a known byte range, methods such as .chars() make that choice explicit. A Rust char is a Unicode scalar value, which is not always the same as a complete user-perceived letter.
Conclusion
A slice is a borrowed view, not a new owner or a copy. Use &[T] to view consecutive elements such as those in an array, and &str to view valid UTF-8 text. Ranges include their start and exclude their end; array indices count elements, while string indices count bytes and must land on character boundaries. Because a slice remains connected to its source, Rust’s ordinary borrowing rules prevent you from changing that source in a way that would invalidate a view still in use.
Next, you’ll use this distinction to choose between an owned String and a borrowed &str in function interfaces.