Rust index out of bounds — the len is N but the index is M
index out of bounds: len is N, index is M
Verified against The Rust Reference: Index expressions, Rust std docs: slice::get, The Rust Book: Chapter 8 (Common Collections) · Updated June 2026
> quick_fix
You accessed a Vec or slice at an index >= its length. Use `.get(index)` which returns `Option<&T>` instead of panicking. Always check bounds before direct indexing, or use iterators which handle bounds automatically.
let v = vec![1, 2, 3];
// Panics if index >= v.len()
let x = v[5]; // thread 'main' panicked: index out of bounds: the len is 3 but the index is 5
// Safe — returns Option<&T>
match v.get(5) {
Some(x) => println!("got: {}", x),
None => println!("index 5 is out of bounds"),
}What causes this error
Rust's `[]` operator on slices and Vecs performs a bounds check at runtime and panics if the index is out of range. Unlike C/C++ which allow undefined behavior (buffer overflow), Rust always panics cleanly. Common causes: off-by-one in index calculation, assuming a Vec has a certain length without checking, using a hardcoded index on data with variable length, or accessing `last()` element via length minus one on an empty collection.
How to fix it
- 01
step 1
Use .get() for safe access that returns Option
`.get(index)` returns `Some(&T)` if the index is valid, `None` if out of bounds. Never panics. Use it whenever the index might be invalid.
let items: Vec<String> = get_items(); // Safe access if let Some(first) = items.get(0) { println!("First item: {}", first); } else { println!("No items"); } // With a default let first = items.get(0).map(|s| s.as_str()).unwrap_or("default"); - 02
step 2
Use iterators to avoid indexing entirely
Most operations that use indexing can be rewritten with iterators (`iter()`, `enumerate()`, `zip()`, `windows()`, `chunks()`). Iterators never go out of bounds.
let numbers = vec![10, 20, 30, 40, 50]; // BAD — manual indexing risks off-by-one for i in 0..=numbers.len() { // off-by-one: should be 0..numbers.len() println!("{}", numbers[i]); } // GOOD — iterator, no indexing for n in &numbers { println!("{}", n); } // With index for (i, n) in numbers.iter().enumerate() { println!("[{}] = {}", i, n); } - 03
step 3
Check length before indexing
When you need direct indexing, check the length first. Rust's bounds check will still catch bugs at runtime, but explicit checks make the intent clear and allow graceful error handling.
fn get_third(items: &[i32]) -> Option<i32> { if items.len() < 3 { return None; } Some(items[2]) // safe — we've verified len >= 3 } // Or use first(), last(), split_first(), split_last() let v = vec![1, 2, 3]; if let Some(&last) = v.last() { println!("last: {}", last); // never panics on empty } - 04
step 4
Use .first() and .last() instead of indexing for edge elements
`.first()` and `.last()` return `Option<&T>` and never panic on empty slices. Prefer them over `v[0]` and `v[v.len()-1]`.
let v: Vec<i32> = vec![]; // BAD — panics on empty Vec let first = v[0]; // panic! let last = v[v.len() - 1]; // panic! (len() is 0, 0-1 wraps to usize::MAX) // GOOD let first = v.first(); // None let last = v.last(); // None
How to verify the fix
- No direct `v[i]` indexing where `i` is not verified to be < `v.len()`.
- All index access uses `.get()` or iterators in code handling user input or variable-length data.
- `cargo test` passes all test cases including empty-input cases.
Why index out of bounds: the len is N but the index is M happens at the runtime level
Rust's `Index` trait implementation for slices calls `slice::index` which performs a runtime bounds check: if `index >= self.len()`, it calls `panic!` with the specific index and length. This is deliberate — Rust's safety guarantee means no undefined behavior in safe code. Buffer overflows (the C equivalent) allow arbitrary memory reads/writes; Rust's bounds check prevents this class of vulnerability entirely. In release builds, bounds checks are retained (unlike some other languages' 'checked only in debug' approach). The check is one comparison instruction — the performance overhead is negligible for most workloads.
Common debug mistakes for index out of bounds: the len is N but the index is M
- Using `v.len() - 1` as an index without checking that `v` is non-empty — panics with usize underflow when empty.
- Splitting a slice with `split_at(n)` where n > slice.len() — panics.
- Index calculation using i32 arithmetic then casting to usize — negative values cast to huge usize values.
- Off-by-one: `0..=v.len()` in a range instead of `0..v.len()` — the last iteration accesses v[v.len()].
- Accessing a 2D Vec as `v[row][col]` where col >= `v[row].len()` — each inner Vec may have a different length.
When index out of bounds: the len is N but the index is M signals a deeper problem
Persistent index-out-of-bounds panics in Rust code processing user data indicate that the data shape assumptions are embedded in the code rather than validated at the input boundary. A CSV parser that assumes column 5 exists in every row will panic on malformed input. The architectural fix is a typed parsing layer at the input boundary: define a struct with named fields, deserialize into it with `serde`, and let serde validate the shape. If a row is missing a column, serde returns an error that propagates cleanly via `?`. Zero direct indexing in the application logic means zero index-out-of-bounds panics from data shape mismatches.
Editor's take
The index-out-of-bounds panic in Rust is almost always a sign that the code was written thinking in C or Python, where you routinely use integer indices. Idiomatic Rust almost never uses direct indexing — it uses iterators, adapters, and the `.get()` method for anything that might fail. The transformation from index-based to iterator-based code is usually straightforward and produces cleaner, more readable code as a side effect.
The subtle version that trips up Rust developers specifically: `usize` underflow when computing `len - 1` on an empty collection. In C, you'd segfault or get garbage. In Python, you'd get -1 and wrap around (also wrong). In Rust debug mode, you get a clear panic: 'attempt to subtract with overflow'. In release mode with overflow-checks disabled (which isn't the default), you silently get `usize::MAX` — an enormous index that panics immediately on the next access. Rust's default of panicking on integer overflow in debug mode catches this class of bug early.
The pattern I see most in production Rust code reviews: accessing the 'first' and 'last' elements of a collection via `v[0]` and `v[v.len()-1]`. These both panic on empty collections. The idiomatic Rust is `v.first()` and `v.last()`, which return `Option` and compose cleanly with `?` or `unwrap_or`. This is a one-line change that makes the code correct for all inputs, not just non-empty ones. A clippy lint (`clippy::get_first`) flags `v.get(0)` but not `v[0]` directly — so the review has to catch the direct indexing case manually.
By Bikram Nath · Curator · Updated June 2026
Frequently asked questions
Does Rust compile-time catch index out of bounds?
Only for constant indices on fixed-size arrays: `let a = [1, 2, 3]; let x = a[5];` is a compile error. For Vec and slice with runtime indices, bounds checking happens at runtime with a panic. The compiler performs bounds-check elimination (BCE) for indices it can statically prove are in range — for example, `for i in 0..v.len() { v[i] }`. Use `cargo rustc -- -Z mir-opt-level=3` to see BCE in MIR output.
What is unsafe indexing and when should I use it?
`slice.get_unchecked(index)` skips the bounds check. It's an `unsafe` operation — if the index is out of bounds, the behavior is undefined (memory corruption, not a panic). Use it only in performance-critical hot loops where you've proven the index is valid. Never use it in application code. Profile first — the overhead of bounds checks is typically negligible compared to cache misses and branch mispredictions.
Why does subtracting from usize panic with 'attempt to subtract with overflow'?
In Rust debug builds, integer arithmetic panics on overflow. `v.len() - 1` panics when `v.len()` is 0 because usize (an unsigned integer) can't represent -1 — it wraps to `usize::MAX` in release mode, causing an enormous index. Use `v.last()` instead of `v[v.len()-1]`. For safe subtraction, use `v.len().checked_sub(1)` which returns `Option<usize>`.