How to use collect

collect runs an iterator to completion and stuffs the items into the container of your choice (Vec, HashMap, String, even Result). Walk through type annotations, the FromIterator trick, and when not to collect.

The end of the iterator road

Iterators in Rust are lazy. Writing (1..=5).map(|n| n * 2) doesn't actually multiply anything. It builds a chain of structs that promise to multiply when poked. To force the chain to actually run and produce something useful, you need a finalizer. The most common finalizer is collect.

The first time you write collect, it feels almost magical. You hand it an iterator and say "give me a Vec." It does. Then you change your mind and ask for a HashSet. It does that too. Same call, same iterator, totally different result. How does the same method know to build different containers? That's the trick worth understanding, because once you see it, collect stops feeling magical and starts feeling like one of Rust's nicest pieces of API design.

What collect actually does

collect is a method on every iterator. Its full signature looks like this:

fn collect<B: FromIterator<Self::Item>>(self) -> B

Ignore the noise and read the important part. B is whatever container you want at the end. B must implement a trait called FromIterator<Self::Item>. Any type that can be built up by feeding it items one at a time can implement that trait. Vec<T>, HashMap<K, V>, HashSet<T>, String, BTreeMap<K, V>, even Result<Vec<T>, E> and Option<Vec<T>>.

That's the whole story. collect doesn't know about Vec or HashMap. It says "I'll hand items one by one to whoever you ask me to build." The recipient does the work.

Because Rust can't read your mind, it needs to know which B you want. There are two clean ways to tell it:

  1. Annotate the binding: let v: Vec<i32> = iter.collect();
  2. Use the turbofish: let v = iter.collect::<Vec<i32>>();

The turbofish (::<...>) attaches type arguments directly to the call. It's especially useful when there's no binding to annotate, like inside an expression.

Minimal examples

A Vec from a range:

fn main() {
    // The annotation on `numbers` tells collect which container to produce.
    let numbers: Vec<i32> = (1..=5).collect();
    println!("{:?}", numbers);   // [1, 2, 3, 4, 5]
}

The same iterator, but a different container:

use std::collections::HashSet;

fn main() {
    // Turbofish form: ::<HashSet<i32>> tells collect what to build.
    // No `let` annotation needed.
    let unique = (1..=5).chain(3..=7).collect::<HashSet<i32>>();
    println!("{:?}", unique);    // {1, 2, 3, 4, 5, 6, 7}
}

A String from an iterator of char:

fn main() {
    // String implements FromIterator<char>, so collecting a char iterator works.
    let shouted: String = "hello".chars().map(|c| c.to_ascii_uppercase()).collect();
    println!("{}", shouted);     // HELLO
}

A HashMap from pairs:

use std::collections::HashMap;

fn main() {
    // collect can build a HashMap from any iterator of (K, V) tuples.
    let scores: HashMap<&str, i32> = [("alice", 10), ("bob", 7)]
        .into_iter()
        .collect();
    println!("{:?}", scores);
}

The interesting thing in each case is the same: the iterator chain doesn't change, only the destination type does.

What error you get if you forget the type

If the compiler can't figure out what B should be, it stops you with a specific message:

error[E0282]: type annotations needed
  --> src/main.rs:3:9
   |
3  |     let v = (1..=5).collect();
   |         ^   --------------- type must be known at this point
   |
help: consider giving `v` an explicit type
   |
3  |     let v: Vec<i32> = (1..=5).collect();
   |          ++++++++++

Fix it by annotating the binding or using the turbofish. The compiler usually suggests the right shape.

A more realistic example: parsing lines into a typed Vec

A common pattern: read lines of input, try to parse each one as a number, and gather the results. Notice how collect interacts with Result.

fn main() {
    let lines = ["1", "22", "333", "oops", "4"];

    // Each parse returns Result<i32, ParseIntError>.
    // Collecting into Result<Vec<i32>, ParseIntError> stops at the first error
    // and returns it. If everything parses, you get Ok(Vec<i32>).
    let parsed: Result<Vec<i32>, _> = lines.iter().map(|s| s.parse::<i32>()).collect();
    match parsed {
        Ok(nums) => println!("all good: {:?}", nums),
        Err(e) => println!("failed: {}", e),
    }
}

This is one of the prettiest tricks in the standard library. There's a FromIterator impl on Result<C, E> that short-circuits on the first Err. You don't have to write the loop yourself. Same trick works for Option<Vec<T>>: stops at the first None.

If you want to keep going past errors instead of bailing, use filter_map(Result::ok) or .flatten() first:

// Drop the bad ones, keep the good ones.
let good: Vec<i32> = lines.iter().filter_map(|s| s.parse().ok()).collect();

Different collection target, same finalizer.

Walking through what happens

Behind the curtain, every FromIterator implementation calls IntoIterator on the source and walks it to completion. For Vec, the impl roughly looks like:

impl<T> FromIterator<T> for Vec<T> {
    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
        let mut v = Vec::new();
        for item in iter {
            v.push(item);
        }
        v
    }
}

That's all collect is on the receiving end: a loop that pushes. The lazy iterator chain wakes up when this loop pulls items. Everything you wrote upstream (map, filter, take, chain) runs as items flow.

The size_hint optimization

When the iterator can tell collect how many items to expect, the destination can pre-allocate. Iterators implement size_hint() which returns (lower_bound, Option<upper_bound>). Vec::from_iter uses the upper bound to call with_capacity first, so it doesn't have to keep reallocating.

The upshot: collecting from a known-length iterator (like a slice or a Range) is more efficient than collecting from a chain that hides the size (like one with flat_map). Most of the time you don't need to think about it.

Common pitfalls

Forgetting the type annotation. The most common compile error around collect. Add a binding type or turbofish.

Collecting when you don't have to. If you only need to iterate, do not collect. Vec allocates. Adapters are zero-cost.

Collecting an iterator of references when you wanted owned values. iter() yields &T. If you need T, call into_iter() on an owned source, or .copied() / .cloned() to convert.

Collecting a HashMap with duplicate keys. The last value wins. If you wanted to count or accumulate, use fold instead of collect.

Collecting into a String from items that aren't char or &str. The compiler will tell you FromIterator<&MyType> is not implemented for String. The fix is usually a map(|x| x.to_string()) or formatting first.

When to reach for collect vs alternatives

Use collect when you actually need the items as a concrete container, usually because something else expects one (passing to a function, returning from one, indexing, sorting in place).

Reach for for_each if you only want side effects: writing to a file, logging each item, sending each into a channel. No allocation, just iteration.

Use fold or reduce when the result is one value, not a container. Sums, products, running tallies, building a single string with separators.

Use for loops when the body has early returns, control flow, or structure that doesn't fit a closure. Iterator adapters are nice; loops are often nicer when logic gets twisty.

If you only need to count matches, use count or filter(...).count(). If you only need to know whether any match exists, use any. Don't collect just to ask a question of the result.

Where to go next

How to Collect an Iterator into Different Collection Types

How to Use Closures with Iterators in Rust

What Is the Difference Between Lazy and Eager Evaluation in Rust Iterators