How to create custom iterator

Create a custom iterator in Rust by defining a struct and implementing the Iterator trait with a next method.

The state machine behind the loop

You're writing a parser for a configuration file. The file has a header line you need to discard, blank lines to ignore, and comments starting with # that should be skipped. You could write a while loop with an index, a boolean flag for the header, and a bunch of if checks. That works until you realize you also need to filter out lines that are too short and map each valid line to a struct. Suddenly your loop is a tangle of state management, indices, and early returns. You want a clean pipeline: read, skip, parse, yield.

A custom iterator turns that tangle into a focused state machine. Instead of managing the loop logic manually, you encapsulate the state and the advancement rules in a struct. The rest of your code just asks for the next item and trusts the iterator to handle the complexity. This pattern keeps your parsing logic isolated and composable. You can chain filters, maps, and folds on top of your iterator without leaking internal state.

Anatomy of an iterator

An iterator in Rust is defined by the Iterator trait. The trait has one required method: next. This method takes &mut self, updates the internal state, and returns Option<T>. The Option is the contract. Some(value) means there is an item. None means the stream is exhausted. The for loop is just syntactic sugar that calls next repeatedly until it sees None.

The trait also requires an associated type called Item. This tells the compiler what type of values the iterator produces. You declare it with type Item = T;. The next method returns Option<Self::Item>. Using Self::Item is the convention; it references the associated type defined in the impl block, keeping the signature generic and tied to the struct.

struct Counter {
    current: u32,
    limit: u32,
}

impl Iterator for Counter {
    // Declare the type of items this iterator yields.
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        // Check the termination condition.
        if self.current < self.limit {
            let result = self.current;
            self.current += 1;
            Some(result)
        } else {
            // Signal exhaustion.
            None
        }
    }
}

fn main() {
    let counter = Counter { current: 0, limit: 3 };
    for num in counter {
        println!("{num}");
    }
}

The &mut self signature is mandatory for stateful iterators. The iterator must be able to change its state between calls. If you try to call next on an immutable reference, the compiler rejects you with E0596 (cannot borrow as mutable). The for loop handles this automatically by taking ownership of the iterator or borrowing it mutably, so you rarely see this error in practice unless you're calling next manually.

Convention aside: Always name your iterator struct with a noun that describes the stream, like Counter, Lines, or ChunkIterator. Avoid names like CounterIterator or CounterIter. The Iterator trait implies the behavior; the name should describe the data.

Laziness is the default

Iterators in Rust are lazy. Creating an iterator does zero work. No items are generated. No state is advanced. The work happens only when next is called. This design enables chaining iterators without intermediate allocations. You can build a pipeline like counter.filter(...).map(...).take(...) and the entire chain is just a description of work. The actual computation happens at the sink, usually a for loop or a collect() call.

This laziness is a performance feature. If you chain ten adapters, Rust doesn't create ten temporary vectors. It creates a single pipeline object that executes the logic item by item. The compiler inlines the calls, and the result is often faster than a hand-written loop because the optimizer sees the whole pipeline.

fn main() {
    // This creates the pipeline but does no work.
    let pipeline = Counter { current: 0, limit: 100 }
        .filter(|n| n % 2 == 0)
        .map(|n| n * n);

    // Work happens here, item by item.
    for val in pipeline {
        println!("{val}");
    }
}

Trust the laziness. If you need to force evaluation, use collect() or iterate. If you're debugging and nothing is happening, check that you're actually consuming the iterator. A common mistake is building a chain and forgetting to iterate, leaving the code silent.

Real-world parser

A realistic iterator often wraps a collection or an I/O stream and applies complex filtering logic. The LogParser example shows how to skip invalid lines and yield only valid entries. The implementation uses a loop inside next to skip items internally. This keeps the caller simple; the caller only sees valid lines.

struct LogParser {
    lines: Vec<String>,
    index: usize,
}

impl Iterator for LogParser {
    type Item = String;

    fn next(&mut self) -> Option<Self::Item> {
        loop {
            // Use get to safely access the line.
            // The ? operator propagates None if the index is out of bounds.
            let line = self.lines.get(self.index)?;
            self.index += 1;

            let trimmed = line.trim();

            // Skip empty lines and comments.
            if trimmed.is_empty() || trimmed.starts_with('#') {
                continue;
            }

            // Yield the valid line.
            return Some(trimmed.to_string());
        }
    }
}

fn main() {
    let logs = vec![
        "# Header".to_string(),
        "".to_string(),
        "INFO: Started".to_string(),
        "# Comment".to_string(),
        "ERROR: Failed".to_string(),
    ];

    let parser = LogParser {
        lines: logs,
        index: 0,
    };

    // Chain adapters on top of the custom iterator.
    for entry in parser.filter(|line| line.starts_with("ERROR")) {
        println!("Alert: {entry}");
    }
}

The ? operator inside next is a common pattern. If get returns None, the ? returns None from next, signaling the iterator is done. This avoids manual bounds checking and keeps the termination logic clean. The loop with continue handles skipping. The iterator advances the index and loops back until it finds a valid item or exhausts the input.

Convention aside: Keep the next method fast. Avoid allocations if possible. If you're returning owned strings, consider returning &str with lifetimes instead, unless the iterator owns the data and you need to yield owned values. Cloning in next can hurt performance in tight loops.

References, lifetimes, and E0507

Returning references from an iterator introduces lifetimes. If your iterator holds a Vec<String> and you want to yield &str, you need to tie the lifetime of the reference to the lifetime of the data. This requires lifetime parameters on the struct and the impl block.

struct Lines<'a> {
    text: &'a str,
    cursor: usize,
}

impl<'a> Iterator for Lines<'a> {
    type Item = &'a str;

    fn next(&mut self) -> Option<Self::Item> {
        if self.cursor >= self.text.len() {
            return None;
        }

        let start = self.cursor;
        // Find the next newline or end.
        let end = self.text[start..]
            .find('\n')
            .map(|offset| start + offset)
            .unwrap_or(self.text.len());

        self.cursor = end + 1;
        Some(&self.text[start..end])
    }
}

The lifetime 'a ensures the references live as long as the original text. If you try to return a reference without lifetimes, the compiler rejects you with E0507 (cannot move out of borrowed content) or a lifetime error. The iterator borrows the data, so the references are valid only while the data exists.

If you need to yield owned data from borrowed input, you must clone or allocate. This is a trade-off. References are zero-cost but require lifetime management. Owned values are easier to use but cost memory. Choose based on your use case. If the iterator is short-lived and the data is large, references win. If the iterator outlives the data or you need to store the items, owned values win.

Don't fight the borrow checker with raw indices. If you need to yield references, add a lifetime. If you need to yield owned data, clone or take. The compiler guides you to the right choice.

Pitfalls and compiler signals

Custom iterators expose a few common traps. The compiler usually catches them, but the errors can be opaque if you don't know what to look for.

If you forget to advance the state, you get an infinite loop. The next method must change the state or return None. If the state never changes and the condition never fails, the iterator loops forever. Add a debug print in next during development to verify advancement.

If you try to move data out of the iterator without consuming it, you hit E0507. The next method takes &mut self, so you can't move fields out of self. You must clone, copy, or use Option::take to extract values. The take pattern is useful for iterators that yield owned items from an Option field.

struct SingleItem {
    item: Option<String>,
}

impl Iterator for SingleItem {
    type Item = String;

    fn next(&mut self) -> Option<Self::Item> {
        // Take the item out, leaving None behind.
        self.item.take()
    }
}

The take method replaces the value with None and returns the original value. This avoids moving out of borrowed content and handles the one-shot semantics cleanly.

If you try to iterate over a struct that doesn't implement Iterator, you get E0277 (the trait Iterator is not implemented for MyStruct). This happens when you forget the impl block or misspell the trait. The compiler suggests implementing the trait if it detects a similar structure.

Treat the compiler errors as hints. E0507 means you're moving data incorrectly. E0596 means you need mutability. E0277 means you're missing the trait impl. Fix the root cause, and the code compiles.

When to write a custom iterator

Custom iterators are powerful but add boilerplate. Use them only when the standard adapters can't express your logic cleanly. The standard library provides map, filter, enumerate, zip, flat_map, and many others. Composing these adapters often covers 90% of use cases.

Use a custom Iterator when you have stateful logic that doesn't fit standard adapters, like parsing a stream with complex skip rules or generating a mathematical sequence. Use IntoIterator when you want your struct to work directly in for loops, allowing the struct to be consumed or borrowed. Use FromIterator when you want to construct your struct from an existing iterator, enabling collect(). Reach for standard adapters like map, filter, and enumerate when your logic is a composition of simple transformations; writing a custom iterator for a simple filter is redundant.

Write the custom iterator only when the standard adapters can't express your logic without leaking state or hurting performance. The boilerplate is worth it when it encapsulates complexity and enables clean composition.

Where to go next