Slices

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

  1. Use .get() / .split_at_checked() for fallible bounds:
    • slice.get(i) and slice.get(start..end) return None instead of panicking, including when a &str range falls inside a multi-byte character.
    • slice.split_at_checked(mid) and slice.split_at_mut_checked(mid) return Option<(&[T], &[T])>.
  2. Prefer iterators (for x in slice) or slicing before a loop: Iterating directly over slice.iter() or asserting assert!(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é")