How to Find an Element in a Vector in Rust

Find elements in a Rust vector safely using the get() method for indices or iter().find() for values.

When the index is a guess and the value is a mystery

You're building a game leaderboard. You have a Vec<u32> of scores. A player asks, "Did I get a score of 42?" or "What's the score at rank 5?" In Python, you grab scores[5] and hope for the best. In Rust, that hope gets you a panic at runtime. You need a way to ask the vector for data without crashing the whole program when the answer is "nothing there."

Rust vectors don't just hand over data. They hand over a promise wrapped in an Option. get(index) checks bounds. It returns Some(&value) or None. iter().find(predicate) walks the list until the predicate matches. It returns Some(&value) or None. The vector protects itself. You have to handle the "not found" case explicitly.

The safe search contract

Think of a vending machine. The standard indexing operator [] is a machine that explodes if you ask for a slot that's empty. It assumes you know exactly what's inside. If you're wrong, the program terminates.

get() is a vending machine with a polite clerk. You ask for slot 5. The clerk checks the inventory. If slot 5 has a snack, the clerk hands you a reference to it. If slot 5 is empty, the clerk hands you a "Not Found" card. The machine stays intact. You decide what to do with the card.

iter().find() is a searchlight. You describe what you're looking for. The searchlight scans the shelves one by one. The moment it sees a match, it stops and points at the item. If it scans the whole shelf and finds nothing, it reports back with "Not Found."

Both methods return Option<&T>. The Option type forces you to acknowledge that the element might not exist. You cannot use the value without unwrapping the Option. This eliminates null pointer exceptions and out-of-bounds crashes at the source.

Minimal examples

Here's the core API. get takes an index. find takes a closure that describes the value.

fn main() {
    let scores = vec![10, 20, 30, 40, 50];

    // get() checks bounds safely. Returns Option<&T>.
    // No panic if index is out of range.
    if let Some(score) = scores.get(2) {
        println!("Score at index 2: {}", score);
    }

    // iter().find() searches by value.
    // The closure receives &T. Pattern match &x to extract the value.
    // find() short-circuits: it stops at the first match.
    if let Some(found) = scores.iter().find(|&x| x == 30) {
        println!("Found value: {}", found);
    }
}

Convention aside: When the element type implements Copy (like integers, floats, or booleans), the community prefers .copied() over .cloned() to extract the value from the Option. copied() signals that you're working with a cheap bitwise copy, not a heap allocation. Write let val = scores.get(0).copied().unwrap_or(0); instead of cloned().

What happens under the hood

get(index) performs a single comparison: index < self.len(). If true, it returns a pointer to the element. If false, it returns None. This is a bounds check. The compiler cannot optimize it away unless it can prove the index is within bounds at compile time. In hot loops, LLVM sometimes eliminates redundant bounds checks, but you should never rely on that for correctness. The check is there to keep you safe.

iter().find(predicate) creates an iterator over references. It calls next() on the iterator, passing each reference to your closure. The closure returns true or false. If true, find wraps the reference in Some and returns immediately. It does not continue scanning. This short-circuiting behavior is crucial. If you're searching a million-item vector and the target is at index 0, find does one comparison. It does not touch the rest of the data.

Short-circuiting saves cycles. find stops the moment it wins.

Realistic usage: searching structured data

Vectors rarely hold just primitives. They hold structs. You need to find a user by name, or a transaction by amount. find shines here because the closure can inspect any field.

#[derive(Debug)]
struct User {
    id: u32,
    name: String,
}

fn main() {
    let users = vec![
        User { id: 1, name: "Alice".to_string() },
        User { id: 2, name: "Bob".to_string() },
        User { id: 3, name: "Charlie".to_string() },
    ];

    // Find a user by name.
    // The closure captures &User. We match the name field.
    let target = "Bob";
    if let Some(user) = users.iter().find(|u| u.name == target) {
        println!("Found user with ID: {}", user.id);
    }

    // Sometimes you need the index, not the value.
    // position() is the index version of find().
    // It returns Option<usize>.
    if let Some(index) = users.iter().position(|u| u.name == target) {
        println!("Bob is at index {}", index);
    }

    // Access the last element safely.
    // last() is equivalent to get(len - 1) but reads better.
    if let Some(last) = users.last() {
        println!("Last user: {:?}", last);
    }
}

Convention aside: last() is preferred over get(len - 1) for readability. It expresses intent clearly. The compiler treats them identically. Use last() when you want the tail. Use get() when the index is variable.

Pitfalls and compiler errors

Borrowing conflicts with find

find returns a reference to the element inside the vector. That reference borrows the vector immutably. If you try to mutate the vector while holding that reference, the borrow checker blocks you.

let mut scores = vec![10, 20, 30];

// find() borrows scores immutably.
let found = scores.iter().find(|&x| x == 20);

// This fails. scores is already borrowed by `found`.
// Compiler error E0502: cannot borrow `scores` as mutable 
// because it is also borrowed as immutable.
if found.is_some() {
    scores.push(40); 
}

The fix is to consume the result before mutating. Clone the value, or use the index to mutate later.

let mut scores = vec![10, 20, 30];

// Extract the value or index before mutating.
if let Some(index) = scores.iter().position(|&x| x == 20) {
    // `index` is a usize. The borrow from position() is gone.
    scores.push(40);
    println!("Found at {}, now pushing.", index);
}

The borrow checker catches the dangling reference before it dangles. Handle the result before mutating the source.

find vs contains

find returns the element. contains returns a boolean. If you only care about existence, contains is clearer.

let scores = vec![10, 20, 30];

// find() returns Option<&i32>.
let has_twenty_find = scores.iter().find(|&x| x == 20).is_some();

// contains() returns bool directly.
let has_twenty_contains = scores.contains(&20);

contains is syntactic sugar for find that discards the result. Use contains when you need a yes/no answer. Use find when you need the element.

binary_search requires sorted data

If the vector is sorted, find is wasteful. find is O(n). binary_search is O(log n). But binary_search has a different return type. It returns Result<usize, usize>. Ok(index) means found. Err(insertion_point) means not found, but tells you where the value would go to maintain order.

let mut sorted = vec![10, 20, 30, 40];
sorted.sort();

match sorted.binary_search(&25) {
    Ok(index) => println!("Found at {}", index),
    Err(pos) => println!("Not found. Would be at index {}", pos),
}

Never use binary_search on unsorted data. The results are meaningless. The compiler cannot enforce sortedness. That invariant is on you.

Decision matrix

Use get(index) when the index comes from user input, configuration, or loop counters where bounds aren't statically provable. Use indexing vec[index] when the index is derived from a length check or mathematical invariant, and a panic indicates a bug in your logic. Use iter().find(predicate) when you need to locate an element by its content or a computed property. Use contains(&value) when you only need a boolean answer and don't need to interact with the element. Use iter().position(predicate) when you need the index of the first matching element. Use binary_search when the vector is sorted and performance matters for large datasets.

Where to go next