How to fix Rust E0507 cannot move out of index

Fix Rust E0507 by borrowing with & or cloning instead of moving out of a collection.

The indexer trap

You have a Vec<String> of usernames loaded from a config file. You want to grab the first name and pass it to a logging function that takes ownership. You write let user = users[0];. The compiler rejects you with E0507: cannot move out of index. You're stuck. The value is right there. You just want it. Why can't you take it?

The error message feels like the compiler is being pedantic, but it's actually protecting your program from a memory safety disaster. Rust's indexing operator [] is designed for peeking, not grabbing. When you use vec[i], you are asking to access the element at index i. You are not asking to remove it. If Rust allowed you to move the value out, the collection would be left in a broken state, holding a slot that points to garbage or freed memory. The compiler stops this before it happens.

Why Rust blocks the move

Rust collections like Vec are guardians of memory. They allocate a contiguous block of heap memory and track how many valid items live there. The Vec owns the data. Every element inside the Vec is managed by the Vec. When the Vec goes out of scope, it drops every element and frees the heap buffer.

Imagine the Vec as a row of lockers. Each locker holds a String. The String itself is a small struct containing a pointer to some text on the heap, a length, and a capacity. The Vec owns the locker row. The String owns the text heap.

If you could write let stolen = lockers[0];, you would be moving the String out of the first locker. The String moves to your local variable stolen. The first locker is now empty, or worse, it contains a moved-from value that is invalid. The Vec still thinks it owns a valid String at index 0. When the Vec eventually drops, it tries to drop the String at index 0. But that String is already gone. The Vec might try to free a pointer that you already used, or it might read garbage. This leads to double-frees, use-after-free, or crashes.

Rust forbids moving out of a collection via indexing because the collection must maintain the invariant that all slots from 0 to len contain valid values. Indexing is a read operation, not a removal operation. If you want to remove a value, you must use an operation that explicitly shrinks the collection and updates its length.

The hidden dereference

The root of E0507 lies in how the indexing operator desugars. The syntax vec[i] is not a primitive. It is shorthand for calling the Index trait.

The Index trait looks like this:

pub trait Index<Idx> {
    type Output;
    fn index(&self, index: Idx) -> &Self::Output;
}

Notice the return type. index returns a reference &Self::Output. When you write vec[0], the compiler translates this to *vec.index(0). The index method gives you a reference to the element. The * operator dereferences that reference to get the value.

Dereferencing a reference to a String attempts to move the String out of the reference. Rust does not allow moving out of a borrowed reference. You can only move out of a place where you have ownership. The vec owns the String, not the reference returned by index. The compiler sees the dereference and realizes you are trying to move a value out of a borrow. That is exactly what E0507 reports.

The error is not about indexing. It is about the implicit dereference that tries to move. The fix is to stop the move. You can borrow explicitly, clone the value, or remove it properly.

When indexing works without borrowing

You might have noticed that let x = numbers[0]; works perfectly fine when numbers is a Vec<i32>. No error. No borrow needed. This creates confusion. Why does it work for integers but not strings?

The difference is the Copy trait. Types like i32, f64, bool, and char implement Copy. Copying a Copy type is semantically identical to moving it, but the compiler allows copies freely. When you dereference a reference to a Copy type, the compiler inserts a copy operation instead of a move. The original value remains valid in the Vec because it was copied, not moved.

String does not implement Copy. Moving a String transfers ownership of the heap buffer. Copying a String would require duplicating the heap buffer, which is expensive and not what you usually want. Rust makes you be explicit. If you want a copy of a String, you must call .clone(). If you try to move it out of an index, the compiler blocks you.

This distinction is a convention that pays off. Copy types are cheap. You can pass them around without worrying about ownership. Non-Copy types carry resources. You must decide whether to share them via references, duplicate them via cloning, or transfer ownership via moves. The compiler enforces this decision.

Borrowing: the default fix

In most cases, you do not need ownership of the element. You just need to read it or pass it to a function that accepts a reference. The simplest fix is to borrow the element explicitly.

fn main() {
    let items = vec![String::from("alpha"), String::from("beta")];

    // Borrow the element. Returns &String.
    // The Vec still owns the String.
    let first = &items[0];

    // Use the reference.
    println!("First item: {first}");
}

By adding &, you tell the compiler you want a reference, not the value. The items[0] desugars to *items.index(0), which tries to move. The & wraps that expression, creating a reference to the result. Since you are not moving the value, the Vec remains intact. The String stays in the Vec. The reference first points into the Vec's memory.

This is the idiomatic approach for reading data. References are cheap. They are just pointers with lifetimes. You can pass &items[0] to any function that takes &String or &str.

Convention aside: The Rust community prefers &vec[i] for reading when you are certain the index is valid. If the index might be out of bounds, use vec.get(i). Indexing panics on out-of-bounds access. get returns Option<&T>, allowing you to handle the error gracefully. Use [] when the index is guaranteed by logic. Use get when the index comes from external input or calculation.

Cloning: buying ownership

Sometimes you need an owned value. Maybe you are storing the element in a different data structure, or passing it to a function that takes ownership. In that case, you must clone the value.

fn main() {
    let items = vec![String::from("alpha"), String::from("beta")];

    // Clone the element. Returns String.
    // Allocates new heap memory for the text.
    let first = items[0].clone();

    // first owns a separate String.
    // items still owns the original.
    println!("Cloned: {first}");
}

Cloning duplicates the value. For String, this means allocating a new heap buffer and copying the text. The original String remains in the Vec. You now have two independent Strings. When first drops, it frees its heap buffer. When items drops, it frees the original heap buffer. No double-free. No dangling pointers.

Cloning has a cost. It allocates memory and copies data. Do not clone blindly. If you only need to read the value, borrow it. If you need ownership, clone it. Profile your code if cloning becomes a bottleneck. Sometimes you can refactor to use references instead.

Convention aside: When cloning an Rc<T> or Arc<T>, the convention is to write Rc::clone(&data) instead of data.clone(). This makes it clear that you are incrementing a reference count, not deep-copying the data. For String and Vec, data.clone() is standard because cloning usually implies a deep copy.

Removing: taking the value home

If you need ownership and you do not care about keeping the element in the collection, remove it. Removal transfers ownership and shrinks the collection.

fn main() {
    let mut items = vec![String::from("alpha"), String::from("beta")];

    // Remove the element at index 0.
    // Returns the owned String.
    // Shifts remaining elements left.
    let first = items.remove(0);

    // items now has length 1.
    // first owns the String that was at index 0.
    println!("Removed: {first}");
}

remove takes the value out of the Vec and returns it. The Vec shifts all subsequent elements left to fill the gap and decrements its length. The invariant is preserved: all slots from 0 to len contain valid values. You get ownership without leaving garbage behind.

Removal has a cost. Shifting elements is O(n). Removing from the end is cheap via pop(). Removing from the middle or beginning is expensive for large vectors. If you need to remove many elements, consider using a different data structure or swapping with the last element and popping if order does not matter.

Mutable access: changing without moving

Often, beginners try to move a value out because they want to modify it. They write let x = vec[0]; x.push_str("!");. This fails with E0507. They think they need ownership to mutate. They do not. Mutable borrowing allows in-place modification.

fn main() {
    let mut items = vec![String::from("alpha"), String::from("beta")];

    // Mutable borrow of the element.
    // Returns &mut String.
    let first = &mut items[0];

    // Modify in place.
    first.push_str("!");

    // The change is reflected in the Vec.
    println!("{items:?}");
}

&mut items[0] gives you a mutable reference. You can call mutating methods like push_str directly on the reference. The value stays in the Vec. The Vec remains valid. This is the idiomatic way to update elements.

Mutable references enforce exclusive access. You cannot have other references to the same element while the mutable borrow is active. The borrow checker ensures this. If you try to read items[0] while first is alive, you get E0502: cannot borrow as mutable because it is also borrowed as immutable. This prevents data races and aliasing issues.

Realistic scenario: function boundaries

Errors often appear at function boundaries. You have a collection, and you want to extract a value to pass to a function that takes ownership.

struct Config {
    api_key: String,
    timeout: u32,
}

fn authenticate(key: String) {
    // Function takes ownership of the key.
    println!("Authenticating with key...");
}

fn process_config(config: &Config) {
    // E0507: cannot move out of `config.api_key`
    // authenticate(config.api_key);

    // Fix: Clone the key if you need ownership.
    authenticate(config.api_key.clone());
}

This is the same pattern as indexing. config.api_key is a field access, which returns a reference when config is borrowed. Dereferencing tries to move. The compiler blocks it. The fix is the same: borrow if the function accepts a reference, or clone if it requires ownership.

Sometimes you can refactor the function to accept a reference. If authenticate only needs to read the key, change the signature to fn authenticate(key: &str). Then you can pass &config.api_key without cloning. This avoids allocation and is often the better design.

Pitfalls and conventions

Watch out for these common traps.

Confusing E0507 with E0382. E0382 is "use of moved value". It happens when you move a value and then try to use it again. E0507 happens when you try to move a value out of a place where you do not have ownership. The fixes are different. E0382 requires cloning before the move or restructuring the code. E0507 requires borrowing, cloning, or removing.

Using get when you mean []. vec.get(i) returns Option<&T>. You must unwrap or match. If you are sure the index is valid, vec[i] is cleaner. If you are unsure, get is safer. The community convention is to use get for indices derived from user input or calculations, and [] for indices that are hardcoded or proven by logic.

Blindly cloning. Cloning works, but it allocates. If you are in a tight loop, cloning every iteration can kill performance. Check if you can borrow instead. If the function signature forces ownership, consider changing the signature.

Indexing slices. Slices &[T] also support indexing. The rules are the same. slice[i] tries to move. &slice[i] borrows. slice[i].clone() clones. Slices are views into data. They do not own the elements. You can never move out of a slice. You can only borrow or clone.

Decision matrix

Use &vec[i] when you only need to read the value and the collection must stay intact. This is the cheapest option and the default choice.

Use vec[i].clone() when you need an owned copy and the collection must stay intact. Accept the allocation cost.

Use vec.remove(i) when you need ownership and the element should leave the collection. Accept the shift cost.

Use vec.get(i) when the index might be out of bounds and you want to handle the error gracefully. This returns Option<&T>.

Use vec.into_iter() when you want to consume the entire collection and move all elements out. This turns the Vec into an iterator that yields owned values.

Use &mut vec[i] when you need to modify the element in place. This avoids cloning and keeps the value in the collection.

Indexing is a window, not a door. If you need to take something out, use a method that opens the door. Trust the compiler to keep the collection safe.

Where to go next