What is the difference between iter and into_iter

iter() borrows elements for reading, while into_iter() takes ownership and consumes the collection.

The ownership decision

You have a Vec of log entries. You need to send them to a remote server. After sending, the local buffer is useless and taking up memory. You want to drain it efficiently. Or maybe you just want to peek at the entries to log a summary, but you need the original data intact for the next step. The choice between iter() and into_iter() decides whether your data survives the loop or gets consumed by it.

Rust forces you to make this choice explicit. Other languages let you loop over a collection and magically decide whether you're copying, referencing, or moving based on context. Rust requires you to state your intent. The method you pick defines the ownership transfer. Get it wrong, and the compiler stops you. Get it right, and your code is both safe and efficient.

Borrowing vs moving

Rust's ownership rule says every value has exactly one owner. The owner is responsible for cleaning the value up when its scope ends. Iterators are how you walk through a collection. The method you call determines what happens to that ownership during the walk.

iter() borrows. You get a peek at each element. The collection stays alive. You can read the data, but you cannot take it away. The collection remains usable after the loop finishes.

into_iter() takes ownership. You get the actual elements. The collection is gone. The iterator now owns the data. When the iterator is done, the data is cleaned up. The original variable is invalidated immediately.

Think of iter() like reading a document on a screen. You can see the text, but you can't delete the file. into_iter() is like cutting the pages out of the binder. You hold the pages now. The binder is empty. You can't put the pages back, and you can't read the binder again.

Minimal example

fn main() {
    // Create a vector with three numbers.
    let numbers = vec![1, 2, 3];

    // iter() borrows each element as &i32.
    // The vector remains usable after the loop.
    for n in numbers.iter() {
        println!("Borrowed: {n}");
    }

    // numbers is still valid here.
    println!("Vector still has {} items", numbers.len());

    // into_iter() moves each element out.
    // The vector is consumed and cannot be used again.
    for n in numbers.into_iter() {
        println!("Moved: {n}");
    }

    // This line would cause a compile error.
    // println!("Vector has {} items", numbers.len());
}

The first loop uses iter(). The variable n has type &i32. The loop reads the numbers. The vector numbers is untouched. You can call .len() after the loop.

The second loop uses into_iter(). The variable n has type i32. The loop takes ownership of each number. The vector numbers is consumed. The compiler marks numbers as moved. Any attempt to use numbers after this point is rejected.

What happens under the hood

When you call iter(), Rust creates an iterator that holds a reference to the collection. Each step yields a reference to an element. No data moves. The collection stays put. This is cheap. It's just pointer arithmetic under the hood. The iterator tracks its position and hands you a reference to the current item.

When you call into_iter(), Rust takes ownership of the collection. The iterator now owns the data. Each step yields the actual value. For a Vec, this means the iterator can drop the backing array and hand you the elements one by one. The original variable is invalidated immediately. The compiler tracks this ownership transfer.

The type system enforces this distinction. The iterator returned by iter() yields references. Any chain of iterator adapters preserves that reference type. You get &T all the way down. The iterator from into_iter() yields owned values. You get T. This difference flows through map, filter, and collect. If your pipeline expects owned values, iter() forces you to clone or dereference. into_iter() provides the owned values directly.

Performance: avoiding the clone tax

Cloning is expensive. Moving is cheap. If you have a collection of owned data like Vec<String>, the choice between iter() and into_iter() can impact performance significantly.

fn process_with_clone(data: Vec<String>) {
    // iter() yields &String.
    // We must clone to get String.
    for s in data.iter() {
        let owned = s.clone(); // Allocation and copy.
        store(owned);
    }
    // data is still here, but we paid for clones.
}

fn process_with_move(data: Vec<String>) {
    // into_iter() yields String.
    // No clone needed.
    for s in data.into_iter() {
        store(s); // Move is cheap.
    }
    // data is gone. Zero allocation overhead.
}

fn store(s: String) {
    // Function takes ownership.
    println!("Stored: {}", s);
}

In process_with_clone, the loop borrows each string. The store function requires an owned String. You must call .clone(). This allocates new memory and copies the data. For large strings or many items, this adds up.

In process_with_move, the loop moves each string out. The store function receives the owned string directly. No allocation. No copy. Just a pointer transfer. The performance difference is real. Use into_iter() when you need owned values and the collection is expendable.

The array quirk

Arrays behave slightly differently than vectors. This asymmetry catches developers coming from other languages.

Both the array and a reference to the array implement IntoIterator. for x in [1, 2, 3] moves the integers. for x in &[1, 2, 3] borrows them. The compiler picks the right implementation based on whether you pass the array or a reference.

Vectors lack this symmetry. &Vec<T> does not implement IntoIterator. You must call .iter() explicitly or use &vec in the loop. If you write for x in &vec, the compiler sees the reference and calls .iter() for you. This is a convenience, but it hides the method call. Some teams prefer explicit .iter() for clarity. Others prefer &vec for brevity. Both compile to the same code.

Stick to iter() for vectors to be explicit. The convention aside: for item in &collection is idiomatic shorthand for for item in collection.iter(). The community uses both. Pick one and be consistent.

Pitfalls and compiler errors

The compiler rejects code that tries to move data it doesn't own. If you pass a reference to a function and try to call into_iter(), you get E0507 (cannot move out of borrowed content). You only have a borrow. You can't take ownership. Use iter() instead.

The reverse trap is common. You call into_iter(), loop through the data, then try to use the collection again. The compiler rejects this with E0382 (use of moved value). The collection was consumed. It's gone.

Watch out for the for loop syntax. for item in collection automatically calls into_iter(). This is a convenience that bites you when you expect borrowing. If you want to borrow in a for loop, write for item in collection.iter() explicitly. Or use for item in &collection. The compiler sees the reference and calls iter() for you.

Another pitfall involves HashMap and HashSet. These collections store owned keys and values. iter() yields (&K, &V). into_iter() yields (K, V). If you need to move the keys or values, iter() won't help. You must use into_iter(). The same rule applies. Borrowing gives you references. Moving gives you ownership.

Decision matrix

Use iter() when you need to read elements without taking ownership. Use iter() when the collection must survive the loop. Use iter() when you are passing the collection to a function that only needs a reference. Use iter() when you want to avoid cloning and can work with references.

Use into_iter() when you want to move elements out of the collection. Use into_iter() when the collection is no longer needed and you want to avoid cloning. Use into_iter() when you are consuming a buffer or queue and want to transfer ownership of each item to a consumer. Use into_iter() when you need owned values and the collection is expendable.

Use iter_mut() when you need to modify elements in place. Use iter_mut() when you want to update values without moving them or cloning the collection. Use iter_mut() when you have mutable access to the collection and need to change its contents.

Match the iterator to the lifetime of your data. Read-only? Borrow. Consuming? Move. Modifying? Mutate. The compiler enforces these rules strictly. If the code compiles, the ownership transfer is correct. Trust the error messages. They tell you exactly where ownership broke.

Where to go next