How does borrowing work with iterators

Iterators borrow collections to process items safely without taking ownership, using .iter() for reading, .iter_mut() for writing, or .into_iter() to consume.

The three ways to look at a collection

You have a Vec of user scores. You write a loop to double every score and print the total. The code compiles. You run it. It works. Then you try to print the original scores after the loop to debug a discrepancy, and the compiler rejects the code with E0382 (use of moved value). The vector is gone. Or you try to filter the list while iterating, and the borrow checker blocks you with E0502 (cannot borrow as mutable because it is also borrowed as immutable).

Iterators are the bridge between "I have data" and "I want to do something with that data." They don't just walk through a collection; they borrow it. The type of borrow determines exactly what you can do, what you can change, and whether the collection survives the loop. Rust gives you three distinct modes. Picking the wrong one leads to compiler errors or accidental data loss. Picking the right one makes the code safe, efficient, and clear.

Borrowing modes explained

Think of a library. You want to access a book.

.iter() is like reading the book in the reading room. You can look at every page. You can take notes on a separate pad. You cannot write in the margins. You cannot take the book home. When you finish, the book stays on the shelf, unchanged. The library still has it.

.iter_mut() is like checking out the book with a special pen. You can write notes in the margins. You can highlight passages. But only you can do it. No one else can read or write in the book while you have it. When you return it, your changes are there. The book stays in the library.

.into_iter() is like buying the book. You take it home. The library no longer has it. If you try to read it at the library later, it's gone. You own the book now. You can read it, modify it, or shred it. The library's copy is destroyed.

In Rust, the collection is the book. The iterator is your access method. The borrow mode defines the rules.

Minimal example

Here are the three methods in action. Each loop does something different to the vector.

fn main() {
    let mut numbers = vec![1, 2, 3];

    // Immutable borrow: read-only access.
    // The Vec stays alive and unchanged after this loop.
    for n in numbers.iter() {
        println!("Read: {}", n);
    }

    // Mutable borrow: can modify elements in place.
    // The Vec stays alive, but contents are updated.
    for n in numbers.iter_mut() {
        *n += 10;
    }

    // Consuming: takes ownership of the Vec.
    // The Vec is moved into the iterator and cannot be used after this.
    for n in numbers.into_iter() {
        println!("Consumed: {}", n);
    }

    // println!("{}", numbers); // Error: use of moved value
}

The first loop yields references &i32. You can read the values. The second loop yields mutable references &mut i32. You can change the values. The third loop yields owned values i32. The vector is consumed.

Convention aside: in a for loop, you rarely write .iter() explicitly. Rust desugars for n in &numbers to for n in numbers.iter(). It desugars for n in &mut numbers to for n in numbers.iter_mut(). And for n in numbers desugars to for n in numbers.into_iter(). The compiler picks the right method based on the reference syntax. Use the sugar. It reads better and does the same work.

What happens under the hood

When you call .iter(), the iterator holds an immutable reference to the collection. Each call to next() returns a reference to an element. The collection remains valid. Multiple immutable iterators can exist simultaneously. You can have three loops reading the same vector at the same time, as long as none of them mutate.

When you call .iter_mut(), the iterator holds a mutable reference to the collection. Each call to next() returns a mutable reference to an element. Only one mutable iterator can exist. The borrow checker enforces this. If you try to create a second mutable iterator, or an immutable one while the mutable one is active, you get E0502. The mutable borrow covers the whole collection.

When you call .into_iter(), the iterator takes ownership. The collection is moved. The iterator holds the collection internally. Each call to next() returns an owned element. The collection is gone from the original variable. You cannot access the original variable again.

The iterator is a cursor, not a copy. The borrow mode dictates what the cursor can touch.

Realistic scenario: updating game entities

You're building a game. You have a list of enemies. Each frame, you need to move every enemy, then check if any enemy hit the player.

struct Enemy {
    x: i32,
    y: i32,
    active: bool,
}

fn update_frame(enemies: &mut Vec<Enemy>, player_x: i32) {
    // Move all enemies.
    // We need mutable access to change coordinates.
    for enemy in enemies.iter_mut() {
        enemy.x += 1;
    }

    // Check collisions.
    // We only need to read data now.
    // The mutable borrow from the previous loop has ended.
    for enemy in enemies.iter() {
        if enemy.active && enemy.x == player_x {
            println!("Player hit!");
        }
    }
}

The first loop borrows the vector mutably. It modifies the x coordinate of each enemy. The loop ends. The mutable borrow is released. The second loop borrows the vector immutably. It reads the data. This works because the borrows don't overlap. The scope of the loop controls the lifetime of the borrow.

If you tried to hold a reference to an enemy from the first loop and use it in the second, the compiler would reject you. The mutable borrow must end before the immutable borrow begins. Let the loop scope do the cleanup for you.

Pitfalls and compiler errors

Accidental consumption

The most common mistake is using .into_iter() when you need the collection later. You write a loop, process items, then try to use the vector again. The compiler rejects this with E0382 (use of moved value). The vector was moved into the iterator. It no longer exists.

Fix: use .iter() or .iter_mut(). Or use the sugar for x in &vec. If you need owned values but want to keep the collection, you must clone. Cloning copies the data. It's expensive. Only clone when you truly need independent ownership.

Modifying structure while iterating

You cannot change the length of a collection while iterating it. You cannot push, pop, or remove items. The iterator holds a borrow of the collection. Modifying the structure might reallocate memory, invalidating the iterator. Rust prevents this.

If you try to push to a vector while iterating, you get E0502. The iterator holds a borrow. Pushing requires a mutable borrow of the whole vector. The borrows conflict.

Fix: collect the changes first, then apply them. Or use methods like .retain() that handle the iteration and modification safely. .retain() takes a closure and removes items that don't satisfy the predicate. It manages the borrow internally.

The &Vec trap

If you pass a &Vec<T> to a function and call .into_iter(), you get &T, not T. This catches many developers off guard.

fn process(items: &Vec<i32>) {
    // This yields &i32, not i32.
    // &Vec implements IntoIterator to yield references.
    for item in items.into_iter() {
        println!("{}", item);
    }
}

The reason is safety. You cannot move values out of a borrowed collection. Moving would leave the collection in an invalid state. The IntoIterator implementation for &Vec yields references. The same applies to slices. &[T] always yields &T when iterated. You can't consume a slice.

Fix: if you need owned values, the caller must provide ownership. Change the function signature to take Vec<T> or use .iter().cloned() if the items implement Clone. The convention is to accept slices &[T] for read-only access. Slices are more flexible. They work with vectors, arrays, and sub-slices.

The slice surprise

Slices behave differently from vectors regarding consumption. A slice is a view. It doesn't own the data. You can't consume a slice. .into_iter() on a slice yields references. .into_iter() on a vector yields owned values.

let vec = vec![1, 2, 3];
let slice: &[i32] = &vec;

// Yields &i32.
for n in slice.iter() {
    println!("{}", n);
}

// Also yields &i32.
// Slices cannot be consumed.
for n in slice.into_iter() {
    println!("{}", n);
}

This is consistent. References always yield references. Ownership yields values. If you have a reference, you can't get owned values without cloning.

If you need the collection later, never use into_iter. The compiler will remind you, but the fix is just adding an ampersand.

Decision matrix

Use .iter() when you only need to read the data and want the collection to remain available afterward. Use .iter_mut() when you need to modify the elements inside the collection without changing the collection's structure. Use .into_iter() when you want to take ownership of the items, or when the collection is no longer needed after the loop. Reach for for x in &collection instead of .iter() for cleaner syntax in loops; the compiler handles the conversion automatically. Reach for for x in &mut collection instead of .iter_mut() for the same reason; it reads better and does the same work. Reach for for x in collection when you want to consume the collection and extract owned values.

Pick the narrowest access you need. Immutable is safer. Mutable is faster for updates. Consuming is efficient for one-offs.

Where to go next