Skip to content
Select theme

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.

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.

10
20
30
thread 'main' (15872116) panicked at src/main.rs:4:24:
index out of bounds: the len is 3 but the index is 3
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

The 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”
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 default

With 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.

None
Some(10)
None
Some(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.

Next in this chapter: what the difference between an array and a Vec is.