The chat log problem
You are building a chat client. The backend dumps a massive vector of messages into your memory. You need to show only the messages from "Alice" in the sidebar. You grab the vector, write a loop, check the sender, and push matches to a new vector. It works. It is also verbose, allocates a new vector manually, and misses the idiomatic Rust way.
Rust gives you a better tool. You chain methods to describe what you want, and the compiler generates the loop for you. The pattern is .filter() followed by .collect(). This creates a pipeline that streams through your data, keeps what matches, and gathers the result.
The iterator pipeline
Rust does not mutate vectors in place for filtering by default. It builds a pipeline. You take the vector, turn it into an iterator, run it through a filter, and collect the survivors into a new vector.
Think of a factory assembly line. The vector is a bin of raw parts. The iterator is the conveyor belt that feeds parts one by one. The filter is a quality inspector standing next to the belt. The inspector looks at each part and shouts "keep" or "trash". The collector is a box at the end of the line that catches only the parts marked "keep".
The key insight is laziness. Writing .filter() does not run the loop. It just places the inspector on the line. Nothing happens until you call .collect(). At that point, the conveyor belt starts, parts flow through, and the box fills up. This laziness lets you chain multiple operations without creating intermediate vectors. You can filter, transform, and take the first ten items in one pass, and Rust only processes ten items.
Minimal example
Here is the pattern in action. You have a vector of numbers. You want only the even ones.
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6];
// .iter() borrows the vec and yields &i32 items.
// .filter() takes a closure that receives &&i32.
// Destructure &&n to get the value inside.
// .collect() consumes the iterator and builds a new Vec.
let evens: Vec<i32> = numbers
.iter()
.filter(|&&n| n % 2 == 0)
.collect();
println!("{:?}", evens); // [2, 4, 6]
}
The closure |&&n| n % 2 == 0 is the filter logic. It receives a reference to a reference because .iter() yields &i32, and the closure argument is a reference to that item. The &&n syntax destructures both layers to give you the integer n. The closure must return a bool. true keeps the item. false drops it.
Convention aside: You will often see let _: Vec<_> = ... in Rust code. The _ tells the compiler to infer the type. This is the community shorthand for "I know this is a Vec, figure out the rest." It saves typing and keeps the code clean.
Give the compiler the type hint. let _: Vec<_> = ... is your friend.
How the compiler sees it
When you write this code, the compiler checks the closure signature. It ensures your closure returns a bool. If you return an i32, you get a type mismatch error. The compiler also checks that the closure does not capture anything that violates borrowing rules.
At runtime, .iter() creates a handle to the first element. .filter() wraps that handle with your closure. .collect() pulls items one by one. For each item, it calls the closure. If the closure returns true, the item gets pushed to a new allocation. The original vector stays untouched.
The compiler needs to know the target type for .collect(). The collect method can produce a Vec, a HashSet, a String, or many other types. Without a type annotation, the compiler cannot decide. You will see error E0283 (type annotations needed) if you omit the Vec<i32> part. The fix is always to add the type.
Trust the type annotation. The compiler is not guessing; it is waiting for your instruction.
Real-world filtering
Real code rarely filters raw integers. You filter structs. Here is a realistic example with a user list.
#[derive(Debug)]
struct User {
name: String,
active: bool,
}
/// Returns a new vector containing only active users.
/// Consumes the input vector since we do not need the original.
fn get_active_users(users: Vec<User>) -> Vec<User> {
users
// into_iter() moves the values out of the vec.
// This avoids cloning and is efficient when the source is disposable.
.into_iter()
.filter(|u| u.active)
.collect()
}
fn main() {
let all_users = vec![
User { name: "Alice".to_string(), active: true },
User { name: "Bob".to_string(), active: false },
User { name: "Charlie".to_string(), active: true },
];
let active = get_active_users(all_users);
println!("{:?}", active);
}
This function uses .into_iter() instead of .iter(). The difference matters. .iter() borrows the vector and yields references. The result would be Vec<&User>. .into_iter() consumes the vector and yields owned User values. The result is Vec<User>. If you do not need the original vector after filtering, use .into_iter(). It moves the data without cloning, which is faster and uses less memory.
Convention aside: The community treats .into_iter() as a signal that the function takes ownership. If you see a function accepting Vec<T> and calling .into_iter(), you know the caller cannot use that vector afterward. This makes data flow explicit.
Use into_iter to move ownership. It signals that the source is consumed.
Pitfalls and compiler errors
Filtering trips up beginners in three specific ways. Knowing the errors saves time.
Missing type annotation
The most common error is E0283. You write .collect() and the compiler complains it cannot infer the type.
let result = numbers.iter().filter(|&&n| n % 2 == 0).collect();
// Error[E0283]: type annotations needed
// cannot infer type for type parameter `B`
The fix is to add the type. let result: Vec<i32> = .... Or use the shorthand let result: Vec<_> = ... if the compiler can infer the inner type from context.
Closure returns the wrong type
The filter closure must return bool. If you return the value itself, you get E0308 (mismatched types).
let _ = numbers.iter().filter(|&&n| n).collect();
// Error[E0308]: mismatched types
// expected `bool`, found `i32`
The closure |&&n| n returns the integer. The compiler expects a boolean. Fix it by adding the condition: |&&n| n > 0.
Borrow conflicts
If you hold a mutable reference to the vector while trying to filter it, you get E0502. You cannot borrow as mutable and immutable at the same time.
let mut numbers = vec![1, 2, 3];
let sum = &mut 0;
// Error[E0502]: cannot borrow `numbers` as immutable because it is also borrowed as mutable
let evens = numbers.iter().filter(|&&n| n % 2 == 0).collect();
*sum += evens.len();
The mutable borrow of sum is not the issue here; the issue is usually holding a mutable ref to the vec or an element while iterating. The fix is to drop the mutable borrow before the filter, or use .into_iter() to take ownership.
Do not fight the borrow checker here. Drop the mutable reference before you start the pipeline.
Filter and transform: filter_map
Sometimes you need to filter and transform in one step. For example, you have a vector of strings that might be numbers. You want to parse them, keep only the valid ones, and convert them to integers.
You could filter first, then map. That runs two passes. Or you can use .filter_map(). This method takes a closure that returns Option<T>. If the closure returns Some(value), the value is kept. If it returns None, the item is filtered out.
fn main() {
let inputs = vec!["1", "two", "3", "four"];
// filter_map returns Option<i32>.
// parse returns Result, so we use ok() to convert Err to None.
let numbers: Vec<i32> = inputs
.iter()
.filter_map(|s| s.parse::<i32>().ok())
.collect();
println!("{:?}", numbers); // [1, 3]
}
The closure |s| s.parse::<i32>().ok() attempts to parse. If parsing succeeds, ok() turns the Ok(i32) into Some(i32). If it fails, ok() turns the Err into None. The filter_map handles both the filtering and the transformation in a single pass.
One pass beats two. Use filter_map when you filter and transform together.
Splitting the stream: partition
What if you need both the items that match and the items that do not? Filtering gives you the keepers. You would need a second pass to get the rejects. Rust provides .partition() for this. It splits the iterator into two vectors: one for items where the closure returns true, and one for items where it returns false.
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6];
// partition returns (Vec<i32>, Vec<i32>).
let (evens, odds): (Vec<i32>, Vec<i32>) = numbers
.into_iter()
.partition(|n| n % 2 == 0);
println!("Evens: {:?}", evens); // [2, 4, 6]
println!("Odds: {:?}", odds); // [1, 3, 5]
}
This runs a single pass and produces both vectors. It is more efficient than calling .filter() twice.
Splitting is cheaper than two filters. Use partition when you need both the keepers and the rejects.
Decision matrix
Use .filter().collect() when you need a new vector of items that match a condition and you do not care about the original vector. Use .retain() when you want to remove items from the existing vector in place without allocating a new one. Use .into_iter().filter().collect() when you want to consume the source vector and move the values into the result. Use .iter().filter().collect() when you need to keep the source vector alive and only borrow the items. Reach for .filter_map() when you need to filter and transform in one step, especially when parsing or unwrapping options. Reach for .partition() when you need to split the data into two groups based on the same condition.
In-place wins on memory. Use retain when the source is disposable.