What is the difference between iter map and for loop

The `for` loop executes code for each item immediately, while `.map()` transforms items into a new iterator for later consumption.

When the loop runs but the work doesn't

You write a for loop to double a list of numbers. It works. You refactor to .map(), run the code, and the output is empty. The loop ran, but the work didn't happen. Or you see a senior developer replace a 20-line for loop with a one-liner chain and wonder how the compiler knows what to do without slowing down.

The confusion isn't about syntax. It's about when the work happens and what the goal is. A for loop is eager. It executes immediately. .map() is lazy. It describes a transformation but does nothing until you consume the result. Mixing these up leads to silent failures or code that fights the compiler.

Eager loops versus lazy descriptions

A for loop drives an iterator to completion right now. It pulls items one by one and executes the body for each item. The side effects happen immediately. Printing, logging, updating a global counter, or pushing to a vector all occur as the loop runs.

.map() returns a new iterator. It wraps the original iterator and holds a closure that defines how to transform each item. No transformation occurs when you call .map(). The closure is just stored. The work happens only when you consume the iterator, usually by calling .collect() or iterating with a for loop.

Think of a for loop like a worker standing at a conveyor belt, grabbing items and painting them immediately. .map() is like handing a blueprint to a machine that says "paint everything that comes through." The machine doesn't start until you flip the switch to collect the output. You can pass the blueprint around, combine it with other blueprints, or store it for later. The items stay unpainted until consumption.

let numbers = vec![1, 2, 3];

// for loop: executes immediately.
// The println! runs for each item right now.
for n in &numbers {
    println!("Got: {}", n);
}

// .map(): creates a new iterator.
// No multiplication happens yet.
// `mapped` is just a struct holding the logic.
let mapped = numbers.iter().map(|n| n * 2);

// To see the results, you must consume the iterator.
// This drives the iterator and runs the closure.
let result: Vec<i32> = mapped.collect();
println!("Result: {:?}", result);

Don't use map to print. The compiler won't stop you, but your teammates will. Use for or .for_each() for side effects.

Under the hood: the Iterator trait

Rust iterators are built on a single trait: Iterator. The trait defines one method: next(). Calling next() returns Some(item) if there are more items, or None when the iterator is exhausted.

The for loop desugars to a while let loop that calls next() repeatedly. The compiler rewrites your code to this pattern:

let mut iter = numbers.iter();
while let Some(n) = iter.next() {
    println!("Got: {}", n);
}

.map() returns a Map struct. This struct implements Iterator. It wraps the source iterator and stores the closure. When you call next() on the Map, it calls next() on the source, applies the closure to the item, and returns the transformed value.

// Conceptual implementation of Map::next
// This is what the standard library does.
impl<I, F, B> Iterator for Map<I, F>
where
    I: Iterator,
    F: FnMut(I::Item) -> B,
{
    type Item = B;

    fn next(&mut self) -> Option<Self::Item> {
        // Pull from the source iterator
        self.iter.next().map(|item| {
            // Apply the closure
            (self.f)(item)
        })
    }
}

The Map struct is just a wrapper. It adds no overhead. The closure is inlined by the compiler. The loop unrolls. The assembly generated by a .map().collect() chain is often identical to a hand-written for loop. This is the zero-cost abstraction promise. You get the expressive power of functional chains without paying a performance penalty.

Trust the zero-cost abstraction. The compiler turns your chain into tight assembly.

Realistic patterns: chaining and composition

The real power of iterators comes from chaining. You can combine .map(), .filter(), .enumerate(), and other adapters to build complex pipelines. Each adapter returns a new iterator. The chain stays lazy until you call a consumer like .collect(), .count(), or .for_each().

This pattern shines when you need to transform data with multiple steps. Imperative code requires intermediate variables or complex indexing. Iterator chains express the intent directly.

struct User {
    name: String,
    age: u8,
    active: bool,
}

fn get_active_adult_names(users: &[User]) -> Vec<String> {
    // Chain adapters to filter and transform.
    // No intermediate allocations.
    // The closure captures `name` by reference.
    users.iter()
        .filter(|u| u.active && u.age >= 18)
        .map(|u| u.name.clone())
        .collect()
}

fn log_user_stats(users: &[User]) {
    // Use for_each for side effects in a chain.
    // This is idiomatic when the goal is action, not data.
    users.iter()
        .filter(|u| u.active)
        .for_each(|u| println!("Active user: {}", u.name));
}

Convention aside: the community prefers .for_each() over .map() for side effects. Using .map() to print or log is a code smell. It signals that you're using a transformation tool for an action tool. .for_each() makes the intent clear and returns (), preventing accidental misuse.

If you find yourself reaching for an index inside a loop, you're probably fighting the iterator. Switch to .enumerate().

Pitfalls and compiler errors

Lazy evaluation causes specific traps. The most common is using .map() for side effects and wondering why nothing happens. The iterator is created and dropped without consumption. The closure never runs.

let numbers = vec![1, 2, 3];

// This creates an iterator and drops it immediately.
// The println! never runs.
numbers.iter().map(|n| println!("{}", n));

Another trap involves ownership. Iterators often yield references. If your closure tries to move a value out of a reference, the compiler rejects you with E0507 (cannot move out of borrowed content). You must clone the value or use into_iter() to take ownership.

let strings = vec!["a".to_string(), "b".to_string()];

// iter() yields &String.
// The closure tries to move the String out of the reference.
// This fails with E0507.
// let moved: Vec<String> = strings.iter().map(|s| s.clone()).collect();

// Fix: clone explicitly, or use into_iter() if you don't need the source.
let moved: Vec<String> = strings.into_iter().map(|s| s).collect();

Performance pitfalls are rare but exist. Complex closures can prevent inlining. If profiling shows iterator overhead, isolate the bottleneck. Switch to a for loop for that specific section. The compiler optimizes most chains perfectly, but pathological cases exist.

Treat the compiler error as a map. E0507 tells you exactly where ownership is breaking. Fix the borrow, don't fight it.

Decision matrix

Use for when you need side effects like printing, logging, or updating external state. Use for when the logic is complex, branching, or requires early returns that break iterator chains. Use for when performance profiling shows iterator overhead is measurable, though this is rare.

Use .map() when you are transforming data into a new collection. Use .map() when chaining operations like filter, flat_map, or enumerate to build a pipeline. Use .map() when you want to express a transformation declaratively without managing indices or intermediate variables.

Reach for .for_each() when you want the functional syntax but the goal is action, not data. Reach for into_iter() when you need ownership of the items and can consume the collection. Reach for iter_mut() when you need to modify items in place.

Pick for when you're debugging and need to inspect state step by step. Pick iterators when the code reads better as a chain. Readability wins.

Where to go next