Slice Bounds and Panics
Every indexing (slice[i]) and range-slicing (&slice[start..end]) operation in Rust performs runtime bounds checking (unless the compiler can prove in advance that the index is in bounds) and panics immediately before any out-of-bounds memory read or write can occur.
Conditions That Trigger a Slice Panic
Unlike C and C++, where reading past the end of an array causes undefined behavior and buffer overreads, Rust validates indices and UTF-8 boundaries at runtime.
| Operation | Panic Condition | Example on s of length 4 |
|---|---|---|
s[i] |
i >= s.len() |
s[4] |
&s[start..end] |
start > end or end > s.len() |
&s[3..2] or &s[1..5] |
&s[start..=end] |
start > end or end >= s.len() |
&s[0..=4] |
&str_slice[start..end] |
start or end is not on a valid UTF-8 character boundary |
&"🦀"[0..1] |
s.split_at(mid) |
mid > s.len() |
s.split_at(5) |
Valid vs. Panicking Boundary Offsets on `s = [10, 20, 30, 40]` (len = 4):
Offsets: 0 1 2 3 4 5
+------+------+------+------+
Elements: | 10 | 20 | 30 | 40 |
+------+------+------+------+
&s[4..4]: ^ (Valid empty subslice `[]` at end!)
s[4]: X (PANIC: index 4 out of bounds for len 4)
&s[2..5]: |<------------X (PANIC: range end 5 out of range for len 4)
Out-of-Bounds Element and Range Panics
Note that an empty range at the exact end of a slice (&s[s.len()..s.len()] or &s[s.len()..]) is valid and produces an empty slice &[], whereas indexing an element at s[s.len()] panics:
fn main() {
let data = [10, 20, 30, 40];
let slice: &[i32] = &data;
// Valid edge case: empty slice at the end boundary `len == 4`
let empty_tail = &slice[4..];
println!("empty_tail (&slice[4..]): {empty_tail:?}");
// Out-of-bounds element access panics with a clear diagnostic message:
let _boom = slice[4];
}
empty_tail (&slice[4..]): []
thread 'main' panicked at src/main.rs:10:17:
index out of bounds: the len is 4 but the index is 4
UTF-8 Character Boundary Panics on &str
Because &str is guaranteed to be valid UTF-8, slicing a string slice in the middle of a multi-byte Unicode scalar value panics at runtime even when the byte indices are < str.len():
fn main() {
let word = "café"; // 'c' (1B), 'a' (1B), 'f' (1B), 'é' (2 bytes: indices 3..5)
println!("Byte length of '{word}': {}", word.len());
// Byte index 4 is inside the 2-byte UTF-8 sequence for 'é' (0xC3 0xA9):
let _invalid = &word[0..4];
}
Byte length of 'café': 5
thread 'main' panicked at src/main.rs:6:25:
byte index 4 is not a char boundary; it is inside 'é' (bytes 3..5) of `café`
Preventing Panics and Eliding Bounds Checks in Loops
- Use
.get()/.split_at_checked()for fallible bounds:slice.get(i)andslice.get(start..end)returnNoneinstead of panicking, including when a&strrange falls inside a multi-byte character.slice.split_at_checked(mid)andslice.split_at_mut_checked(mid)returnOption<(&[T], &[T])>.
- Prefer iterators (
for x in slice) or slicing before a loop: Iterating directly overslice.iter()or assertingassert!(slice.len() >= n)once before a hot loop allows LLVM to eliminate per-element bounds checks inside the loop body completely:
fn main() {
let bytes = [1u8, 2, 3];
let text = "café";
println!("split_at_checked(2): {:?}", bytes.split_at_checked(2));
println!("split_at_checked(9): {:?}", bytes.split_at_checked(9));
println!("text.get(0..4): {:?}", text.get(0..4));
println!("text.get(0..5): {:?}", text.get(0..5));
}
split_at_checked(2): Some(([1, 2], [3]))
split_at_checked(9): None
text.get(0..4): None
text.get(0..5): Some("café")