Why does indexing an array out of bounds panic?
Short answer: indexing an array checks the index against the length at run time, and a
failed check panics — the program stops instead of reading memory it doesn’t own.
array.get(i) runs the same check and hands back None when the index is past the end.
When the index is a constant, the compiler rejects the code before it ever runs.
From a fresh project — cargo new bounds-demo — replace the contents of src/main.rs
with each block below and run it with cargo run. The project name shows up only in
cargo’s own output; nothing else here depends on it. Everything here is Rust 1.99.0,
edition 2024.
Why there’s a check at all
Section titled “Why there’s a check at all”In C, reading past the end of an array reads whatever happens to sit at that address —
no error, wrong value. In Rust that read is undefined behaviour, so the language puts a
check in front of it: compare the index with the length, and stop if it’s past the end.
The check is one comparison on every a[i], on reads and writes alike, and it doesn’t
switch off in release builds. Arrays, slices and Vecs all index through the same slice
code, so the message below is what a Vec prints too.
What the panic looks like
Section titled “What the panic looks like”fn main() { let nums = [10, 20, 30]; for i in 0..=3 { println!("{}", nums[i]); }}102030thread 'main' (15872116) panicked at src/main.rs:4:24:index out of bounds: the len is 3 but the index is 3note: run with `RUST_BACKTRACE=1` environment variable to display a backtraceThe loop prints the three elements, then runs one index too far and dies with exit code
101. The last valid index is len() - 1, so a loop that walks a whole array runs
0..len(), not 0..=len(). The number in brackets after thread 'main' is the OS
thread ID; it changes every run. The location is your indexing line, not one inside the
standard library — the check is #[track_caller] — and the message comes from
panic_bounds_check, the same text whether the index is 3 or 3000.
When the index is a constant, the compiler refuses
Section titled “When the index is a constant, the compiler refuses”fn main() { let nums = [10, 20, 30]; let fourth = nums[3]; println!("{fourth}");}error: this operation will panic at runtime --> src/main.rs:3:18 |3 | let fourth = nums[3]; | ^^^^^^^ index out of bounds: the length is 3 but the index is 3 | = note: `#[deny(unconditional_panic)]` on by defaultWith a literal index on a fixed-size array, the answer is known before the program starts: there’s nothing left to check, and the compiler rejects the code outright. Move the index behind a variable, a function argument or input, and the check moves to run time, where the panic above is what a broken one looks like.
get() runs the same check
Section titled “get() runs the same check”fn main() { let nums = [10, 20, 30]; let i = nums.len(); println!("{:?}", nums.get(i)); println!("{:?}", nums.get(0)); println!("{:?}", nums.get(1..5)); println!("{:?}", nums.first()); println!("{:?}", nums.last());}NoneSome(10)NoneSome(10)Some(30)get does the same comparison and answers with an Option: Some(&value) when the
index is inside, None when it’s past the end — a range that runs off the end included.
Arrays don’t have their own get; it comes from the slice an array coerces to, which is
why the same call works on a Vec, where
its own page covers
the Option combinators. get_mut is the same call with Option<&mut T> back.
Indexing is the right call when your own logic guarantees the index — a loop counter
bounded by len(), a position checked one line earlier. get is for when the index
arrives from outside. Skipping the check altogether is get_unchecked, which is
unsafe for exactly that reason.
Related
Section titled “Related”- How to get an element from a Vec without panicking —
geton aVec, and theOptioncombinators - What is the difference between a Vec and a slice? — the slice an array coerces to
- Working with collections — the rest of this chapter
Next in this chapter: what the difference between an array and a Vec is.