When your loop refuses to start
You build a struct to hold a playlist of songs. You add a method to load tracks. Everything compiles. Then you try to play the music:
for song in my_playlist {
println!("{}", song.title);
}
The compiler rejects you with E0277 (the trait bound Playlist: IntoIterator is not satisfied). You have the data. You have the loop. But Rust refuses to connect them. The error message points to a trait you haven't seen yet: IntoIterator.
This trait is the bridge between your data structure and the for loop. Without it, Rust treats your struct as a black box. It doesn't know how to extract items, how many items exist, or when to stop. Implementing IntoIterator tells the compiler exactly how to turn your type into a stream of values.
The adapter pattern for loops
Rust's for loop is syntax sugar. Under the hood, it doesn't loop over a collection directly. It loops over an iterator. The for loop calls a method to get an iterator, then repeatedly asks that iterator for the next item until there are none left.
IntoIterator is the trait that provides that method. It defines how to convert a type into an iterator. Think of a collection as a sealed box of items. You can't just reach in and grab things one by one. You need a mechanism to open the box and feed the contents onto a conveyor belt. IntoIterator is the machine that opens the box and starts the belt. The iterator is the belt itself.
The trait has three associated types:
Item: The type of value the iterator yields.IntoIter: The type of the iterator returned byinto_iter.into_iter(self) -> Self::IntoIter: The method that consumes the collection and returns the iterator.
The method takes self by value. This is deliberate. The default behavior is to consume the collection. Once you turn the box into a belt, the box is gone. This matches the mental model of moving items out of a container. If you need to keep the collection alive, you implement IntoIterator for references instead.
Minimal implementation
Here is a custom collection that wraps a Vec. To make it loopable, you implement IntoIterator.
use std::iter::IntoIterator;
struct TaskList {
tasks: Vec<String>,
}
impl IntoIterator for TaskList {
// The type yielded by the iterator.
type Item = String;
// The type of the iterator returned by into_iter.
type IntoIter = std::vec::IntoIter<String>;
// Consumes self and returns the inner iterator.
fn into_iter(self) -> Self::IntoIter {
// Delegate to Vec's into_iter, which moves items out.
self.tasks.into_iter()
}
}
fn main() {
let list = TaskList {
tasks: vec!["Compile".to_string(), "Test".to_string()],
};
// This works now. list is consumed.
for task in list {
println!("Running: {}", task);
}
}
The compiler trusts your types. If Item and IntoIter don't match the actual return type of into_iter, the error will be brutal. The associated types must align perfectly with the iterator you return.
What the compiler actually does
When you write for task in list, the compiler desugars this into a loop that uses IntoIterator. The expansion looks roughly like this:
{
// Call into_iter to get the iterator.
let mut iter = list.into_iter();
loop {
// Ask for the next item.
match iter.next() {
Some(task) => {
// Body of the loop.
println!("Running: {}", task);
}
None => {
// Iterator exhausted. Break out.
break;
}
}
}
}
This expansion reveals two important facts. First, into_iter is called exactly once, before the loop starts. Second, the loop relies on the Iterator trait, not IntoIterator. IntoIterator gets you the iterator; Iterator drives the loop. The two traits work together. IntoIterator is the entry point. Iterator is the engine.
The separation exists because consumption and stepping are different concerns. IntoIterator handles the conversion, which might involve consuming the value, borrowing it, or cloning metadata. Iterator handles the stateful progression through items. Keeping them separate allows Rust to support loops over owned values, references, and mutable references with a single syntax.
Realistic example: The three-impl pattern
In real code, you rarely implement IntoIterator only for the owned type. Users expect to loop over references too. The standard library follows a convention called the "three-impl pattern." Collections implement IntoIterator for T, &T, and &mut T. This allows for x in v, for x in &v, and for x in &mut v to all work without the user calling methods manually.
Here is TaskList with all three implementations.
impl<'a> IntoIterator for &'a TaskList {
// Yields references to tasks.
type Item = &'a String;
// Returns a slice iterator over references.
type IntoIter = std::slice::Iter<'a, String>;
fn into_iter(self) -> Self::IntoIter {
// self is &TaskList. tasks is &Vec.
// Call iter() to get an iterator over references.
self.tasks.iter()
}
}
impl<'a> IntoIterator for &'a mut TaskList {
// Yields mutable references.
type Item = &'a mut String;
// Returns a slice iterator over mutable references.
type IntoIter = std::slice::IterMut<'a, String>;
fn into_iter(self) -> Self::IntoIter {
// self is &mut TaskList. tasks is &mut Vec.
// Call iter_mut() to get an iterator over mutable refs.
self.tasks.iter_mut()
}
}
The lifetime 'a ties the iterator to the reference. When you iterate over &TaskList, the iterator yields &String values that live as long as the reference. The compiler enforces this. You cannot return references that outlive the borrow.
Convention aside: The community prefers relying on these implicit IntoIterator implementations over calling .iter() explicitly. Writing for x in &vec is idiomatic. Writing for x in vec.iter() is redundant. The IntoIterator impl for references exists to make the loop syntax ergonomic. Use the loop syntax. Let the compiler call the right method.
Pitfalls and errors
Implementing IntoIterator introduces a few common traps.
If you forget the implementation, you get E0277. The compiler tells you the trait is not satisfied. This happens when you wrap a collection in a struct but don't forward the trait. The fix is to implement IntoIterator and delegate to the inner collection.
If you consume the collection and then try to use it, you get E0382 (use of moved value). This is the most frequent error for beginners. into_iter takes self. The collection is moved into the iterator. After the loop, the variable is gone.
let list = TaskList { tasks: vec!["A".to_string()] };
for task in list {
println!("{}", task);
}
// Error E0382: use of moved value `list`.
println!("List has {} tasks", list.tasks.len());
The fix is to borrow the collection if you need it later. Change for task in list to for task in &list. This triggers the &TaskList implementation, which borrows instead of consuming.
Another pitfall is a type mismatch between IntoIter and the return type of into_iter. If you declare type IntoIter = Vec<String> but return self.tasks.into_iter(), the compiler rejects you. Vec::into_iter returns std::vec::IntoIter<String>, not Vec<String>. The associated type must match the concrete type returned by the method.
Counter-intuitive but true: the more you use unsafe to implement iterators, the harder the rest of your code becomes to reason about. Stick to safe wrappers around standard iterators unless you have a measured performance reason to do otherwise.
Decision: IntoIterator vs Iterator
Rust provides both IntoIterator and Iterator. Knowing which one to implement depends on what you are building.
Use IntoIterator when you want your custom type to work in for loops and iterator adapters like .map() or .filter() that accept IntoIterator. This trait makes your type feel like a native collection.
Use Iterator when you are building the actual stepping logic, defining how to get the next item and when to stop. This trait powers the loop body and chaining methods.
Reach for into_iter() explicitly when you need to consume a collection and extract its elements, often to move them out of a Vec into another structure.
Pick iter() or iter_mut() when you need to borrow the collection instead of consuming it, preserving the original data for later use.
Implement IntoIterator to make your type feel like a native collection. The ergonomics payoff is huge. Users expect to loop over your type without calling methods. Give them that expectation.