The hole in the vector
You're building a function to process a queue of user messages. You grab the first message to log it, then you try to send it to the database. The compiler stops you with E0507 (cannot move out of index of Vec). It feels like Rust is being stubborn. You just want to read a value. Why does it think you're stealing it?
You write let msg = messages[0];. In Python or JavaScript, this copies the reference or the value. In Rust, this syntax attempts to move the value out of the vector. The vector owns the data. Moving it out leaves a gap. Rust's Vec type guarantees contiguous memory with no gaps. If you move an element out, that guarantee breaks. The compiler rejects the code to protect the data structure's integrity.
The fix is usually adding an ampersand. let msg = &messages[0];. This borrows the value. The vector keeps ownership. The gap never forms. The code compiles. But understanding why requires looking at what indexing actually does under the hood.
Indexing is a theft, not a peek
Think of a Vec like a row of lockers. Each locker holds a box. The Vec owns all the lockers and the boxes inside them.
When you write vec[i], you are telling Rust to reach into locker i and pull the box out. The box leaves the locker. The locker is now empty. You hold the box.
If you try to pull the box out again, the locker is empty. There is nothing to pull. Worse, the row of lockers now has a hole. If the lockers are supposed to be a solid wall, a hole is a structural failure. Rust's Vec enforces the rule that the wall must remain solid. You cannot create a hole by moving a box out.
When you write &vec[i], you are telling Rust to look inside locker i without opening it. You get a view of the box. The box stays in the locker. The wall remains solid. You can look as many times as you want.
This distinction matters because Rust treats data access as an ownership transaction. Indexing is a transaction that transfers ownership. Borrowing is a transaction that grants temporary access. The syntax vec[i] is the transfer. The syntax &vec[i] is the access.
The minimal fix
Here is the error in its simplest form. The code tries to move a String out of a Vec. String does not implement Copy, so the move consumes the value.
fn main() {
let items = vec![String::from("Sword"), String::from("Shield")];
// This attempts to move the String out of the Vec.
// The Vec owns the String, so this leaves a hole in the memory.
// The compiler rejects this with E0507.
let first = items[0];
// The Vec is now invalid. The first slot is empty.
// Rust prevents this state from existing.
}
The compiler output points to the indexing operation. The error code E0507 appears with a message like "cannot move out of index of Vec<String>". The fix is to borrow the element.
fn main() {
let items = vec![String::from("Sword"), String::from("Shield")];
// The ampersand borrows the value.
// The Vec retains ownership. No hole is created.
let first = &items[0];
println!("First item: {}", first);
}
Add the ampersand. The compiler knows you just want to look.
Why the compiler screams
The confusion often comes from integers. If you index a Vec<i32>, it works. let x = numbers[0]; compiles fine. This makes beginners think indexing copies by default. It does not.
The difference is the Copy trait. Types like i32, bool, and f64 implement Copy. When you move a Copy type, Rust silently duplicates the bits. The original stays in the vector. The new variable gets a copy. No hole forms.
Types like String, Vec, and custom structs do not implement Copy. Moving them transfers ownership. The original becomes invalid. Indexing a Vec<String> tries to transfer ownership. The vector cannot hold an invalid slot. The compiler blocks the move.
The syntax vec[i] desugars to *vec.index(i). The index method returns a reference &T. The brackets add a dereference *. That dereference is where the move happens.
For Copy types, the dereference copies the bits. For non-Copy types, the dereference moves the value. The hidden dereference is the trap. You see brackets. You think "access". The compiler sees a dereference. It sees a move.
Convention aside: When you see vec[i] in code, ask yourself if the element type implements Copy. If it does, the code copies. If it doesn't, the code moves. This inconsistency is a classic source of bugs. Explicit borrowing removes the ambiguity.
Real world: taking items out
Sometimes you actually want to take the value out. You're building a game inventory. The player equips a sword. The sword must leave the inventory and go to the character's hand. You need ownership of the element.
Indexing cannot do this. Borrowing cannot do this. You need a method that removes the element and returns it. Vec::remove does exactly that.
/// Removes the item at the given index and returns ownership.
/// Shifts all subsequent elements to fill the gap.
fn take_item(inventory: &mut Vec<String>, index: usize) -> String {
// remove() takes ownership of the element.
// It shifts elements to maintain order.
// This is O(N) because of the shift.
inventory.remove(index)
}
fn main() {
let mut inventory = vec![String::from("Sword"), String::from("Shield")];
// Takes ownership of "Sword".
// The Vec shrinks. "Shield" moves to index 0.
let equipped = take_item(&mut inventory, 0);
println!("Equipped: {}", equipped);
println!("Remaining: {:?}", inventory);
}
remove shifts elements. If you remove from the front of a large vector, every element moves down one slot. This is an O(N) operation. Doing this in a loop turns your code into O(N²).
If order doesn't matter, use swap_remove. It swaps the target element with the last element and pops the last slot. No shifting occurs. This is O(1).
fn take_any_item(inventory: &mut Vec<String>, index: usize) -> String {
// swap_remove swaps the target with the last element.
// It pops the last slot. Order changes.
// This is O(1) and much faster for large vectors.
inventory.swap_remove(index)
}
Convention aside: The community calls swap_remove the "performance hacker's friend". Use it whenever order is irrelevant. If you're removing from the middle or end of a vector and order doesn't matter, swap_remove is the standard choice. It signals that you care about speed, not sequence.
If you are removing from the front repeatedly, Vec is the wrong tool. Use VecDeque. It supports O(1) removal from both ends. Switching data structures is better than fighting the layout of a vector.
If you're removing from the front of a Vec in a loop, you're paying an O(N) tax. Pay it once by switching to a VecDeque.
Pitfalls and compiler signals
The E0507 error is clear. It tells you exactly what went wrong. You tried to move out of an index. The fix is almost always borrowing or using a removal method.
Indexing also panics on out-of-bounds access. vec[100] crashes the program if the vector has fewer than 101 elements. In production code, panics are dangerous. Use get to handle missing indices gracefully.
fn safe_access(items: &[String], index: usize) -> Option<&String> {
// get() returns Option<&T>.
// It returns None if the index is out of bounds.
// This avoids panics and lets you handle errors.
items.get(index)
}
get returns a reference wrapped in Option. You can pattern match or use combinators. This is the safe way to access elements when the index might be invalid.
Another pitfall is mutating through an index. vec[i] = value works for Copy types. For non-Copy types, you need vec[i] = new_value where new_value is a fresh value, or you need mutable indexing vec[i] = std::mem::take(&mut vec[i]) to swap out the old value. Mutable indexing is tricky. It's often clearer to use get_mut or iterators.
Convention aside: let _ = vec.remove(0); is a signal to readers. The let _ discards the result. It tells anyone reading the code that you intentionally removed the element and threw it away. Don't just call vec.remove(0); without binding. The discard signal prevents confusion about whether you forgot to use the value.
Trust the borrow checker. It usually has a point.
Choosing your access pattern
Rust offers several ways to access vector elements. The right choice depends on what you need to do with the data.
Use &vec[i] when you need a reference to an element and are certain the index is valid. This is the standard read pattern. It borrows the value and leaves the vector intact.
Use vec.get(i) when the index might be out of bounds and you want to handle the missing case gracefully. This returns Option<&T> and prevents panics.
Use vec.remove(i) when you need to extract ownership of an element and preserve the order of the remaining items. This shifts elements and is O(N).
Use vec.swap_remove(i) when you need to extract ownership and order doesn't matter. This swaps with the last element and is O(1).
Use vec.into_iter() when you want to consume the entire vector element by element. This moves ownership of all values and leaves the vector empty.
Use for item in &vec when you need to read every element. Iteration is clearer and safer than indexing. Indexing is rarely the right tool for loops.
Reach for iteration. Indexing is a sledgehammer when a loop is a scalpel.