What Is the Difference Between iter(), iter_mut(), and into_iter()?

iter() borrows immutably, iter_mut() borrows mutably, and into_iter() consumes the collection.

The three ways to walk a collection

You have a Vec of user records. You need to calculate the total score. You write for user in users. The compiler rejects you. You try users.iter(). It works. Later you need to update the scores. You try the same loop. The compiler rejects you again. You switch to iter_mut(). Now you need to move the users into a new collection, and suddenly iter() won't compile because it only gives you references. You're juggling three methods that look identical but behave completely differently. The difference isn't syntax. It's about who owns the data and what you're allowed to do with it.

Ownership, mutability, and the library analogy

Think of your collection like a book in a library. iter() is reading the book at the desk. You can look at every page, but you can't write in the margins, and you can't take the book home. The library keeps it safe for the next reader. iter_mut() gives you a pencil. You can still read every page, but now you can also write notes in the margins or cross out typos. You're changing the book, but you still can't take it home. The library owns the book. into_iter() is buying the book. You take it home. You can read it, write in it, or even tear out pages. The library no longer has the book. You own it now.

In Rust, iter() borrows immutably. iter_mut() borrows mutably. into_iter() takes ownership. The iterator type tells the compiler exactly what rights you have over the data.

Minimal example

fn main() {
    let mut numbers = vec![10, 20, 30];

    // iter() yields &T. You can read values.
    // The vector remains usable after the loop.
    for n in numbers.iter() {
        println!("Read: {}", n);
    }

    // iter_mut() yields &mut T. You can read and modify values.
    // The vector remains usable after the loop.
    for n in numbers.iter_mut() {
        *n *= 2;
    }

    // into_iter() yields T. You take ownership of each value.
    // The vector is consumed and cannot be used after this loop.
    for n in numbers.into_iter() {
        println!("Owned: {}", n);
    }
}

What happens under the hood

The for loop hides a secret. When you write for x in collection, Rust doesn't call a method on the collection directly. It calls IntoIterator::into_iter(collection). This trait decides which iterator to produce based on the type of the argument. If you pass &collection, it calls iter(). If you pass &mut collection, it calls iter_mut(). If you pass collection by value, it calls into_iter().

This design lets the same syntax handle all three cases. The compiler picks the right method based on how you borrow the collection in the loop header. You can write for x in &vec to read, for x in &mut vec to write, or for x in vec to consume. The trait system routes the call automatically.

The Copy trait can mask this behavior. When you iterate over Vec<i32>, the values are Copy. into_iter() moves the i32 out, but since i32 implements Copy, the move is a bitwise copy. The original data in the vector is technically moved, but you don't feel the pain because integers are cheap to duplicate. When you iterate over Vec<String>, into_iter() moves the String out. The vector is consumed, and the String inside the loop owns the heap data. If you try to use the vector after the loop, the compiler rejects it. Always test your iterator logic with non-Copy types to verify you understand the ownership flow.

The for loop is a convenience wrapper that delegates to IntoIterator. Trust the trait to pick the right path.

Realistic usage

Consider a task manager that processes a batch of work items. You might need to log the items, update their status, and then move them into a database queue. Each step requires a different iterator.

#[derive(Debug)]
struct Task {
    id: u32,
    completed: bool,
}

fn process_batch(tasks: Vec<Task>) {
    // Read-only pass: log all task IDs.
    // Borrowing the vector keeps it alive for the next step.
    for task in &tasks {
        println!("Checking task {}", task.id);
    }

    // Mutable pass: mark even IDs as completed.
    // Mutable borrow allows changing the bool field.
    for task in &mut tasks {
        if task.id % 2 == 0 {
            task.completed = true;
        }
    }

    // Ownership pass: move tasks into a database batch.
    // The vector is consumed here. No cloning occurs.
    let batch: Vec<Task> = tasks.into_iter().collect();
    println!("Batch size: {}", batch.len());
}

Using into_iter() here avoids cloning. If you used iter() and tried to collect into a Vec<Task>, you'd have to clone every Task. Cloning allocates memory and copies bytes. Moving just transfers a pointer. Use into_iter() to avoid unnecessary clones when you're transferring data between collections.

Pitfalls and compiler errors

Trying to modify values through an immutable iterator fails immediately. You write for x in vec.iter() and attempt *x = 5. The compiler rejects this with a "cannot assign to *x" error. The iterator yields &i32, not &mut i32. You need iter_mut() to get a mutable reference.

Using the collection after into_iter() triggers E0382 (use of moved value). You call vec.into_iter() and then try to print vec.len(). The compiler rejects this. into_iter() consumes the vector. The variable is gone. If you need the vector later, borrow it with &vec or &mut vec in the loop.

The & trap in closures catches many developers. When using .map(), the closure argument type matters. vec.iter().map(|x| x + 1) works because x is &i32 and Rust auto-derefs for arithmetic. vec.into_iter().map(|x| x + 1) also works because x is i32. But if you have a struct, vec.iter().map(|s| s.field) accesses the field through a reference, while vec.into_iter().map(|s| s.field) moves the field out of s. Be careful with types that aren't Copy. Moving a field out of a struct leaves the struct in an invalid state unless you use std::mem::replace or similar techniques.

If the compiler complains about moved values, check your loop header. A missing & is the usual culprit.

Decision matrix

Use iter() when you only need to read the values and the collection must remain usable afterward. This is the default for most loops. Use iter_mut() when you need to modify the values inside the collection without taking ownership. The collection stays alive, but you get exclusive access to change it. Use into_iter() when you want to take ownership of the values, or when the collection is no longer needed. This moves the elements out, which is efficient for transferring data to another container. Use &collection in a for loop when you want the compiler to pick iter() automatically. This is the idiomatic way to read without typing .iter(). Use &mut collection in a for loop when you want the compiler to pick iter_mut() automatically. This keeps the syntax clean while granting mutable access.

The community prefers for x in &vec over for x in vec.iter(). Both do the exact same thing. The reference syntax is shorter and signals intent clearly: you are borrowing the vector. You'll see .iter() mostly when chaining methods like .iter().filter().map(). In a standalone loop, the reference wins. Also, into_iter() is the method that makes for x in vec work. When you see a function taking impl IntoIterator, it accepts vectors, slices, references, and mutable references. This flexibility comes from the trait converting the input into the right iterator type.

Pick the iterator that matches your intent. The borrow checker will enforce the rest.

Where to go next