Question

How to Return an Iterator in Rust

Question

I have Rust code like this, which compiles and runs correctly:

fn main() {
    let text = "abc";
    println!("{}", text.split(' ').take(2).count());
}

Then I tried to move the split logic into a separate function:

fn main() {
    let text = "word1 word2 word3";
    println!("{}", to_words(text).take(2).count());
}

fn to_words(text: &str) -> &Iterator<Item = &str> {
    &(text.split(' '))
}

This does not compile, and I am not sure what the correct return type of to_words() should be.

The compiler reports that methods like count() are not available because the trait bounds are not satisfied.

What is the correct way to write to_words() so this works, and what Rust concept am I missing about returning Iterator or other traits from functions?

Short Answer

By the end of this page, you will understand why Iterator itself cannot usually be returned by reference the way a concrete type can, and how Rust handles returning trait-based values. You will learn when to use impl Iterator, when to use trait objects like Box<dyn Iterator<...>>, how lifetimes apply to iterators that borrow data, and why your original function failed.

Concept

In Rust, Iterator is a trait, not a concrete type. That distinction is the key to your question.

When you write:

text.split(' ')

Rust creates a concrete iterator type. That concrete type is something like std::str::Split<'a, char>. It implements the Iterator trait.

Trait vs concrete type

A trait describes behavior:

  • Iterator says a type can produce items one by one.
  • It does not by itself tell Rust how much memory the value needs.

A concrete type describes an actual value in memory:

  • Split<'a, char> has a known size.
  • Rust can return it directly from a function.

That is why this works conceptually:

fn to_words(text: &str) -> std::str::Split<'_, char> {
    text.split(' ')
}

But in practice, Rust programmers usually write:

fn to_words(text: &str) -> impl Iterator<Item = &str> {
    text.split(' ')
}

Why &Iterator<Item = &str> is wrong here

Your function tried to return:

fn to_words(text: &str) -> &Iterator<Item = &str>

This has multiple problems:

1. Iterator is a trait, not a concrete return type

A bare trait type does not have a known size at compile time, so Rust cannot return it by value directly.

2. You returned a reference to a temporary value

This expression:

&(text.split(' '))

creates an iterator temporarily, then tries to return a reference to it. That temporary value is destroyed when the function ends, so the reference would point to invalid memory.

Rust prevents that.

3. Trait object references have limitations

A reference like &dyn Iterator<Item = &str> can exist, but it refers to some iterator value stored elsewhere. Your function does not have such a long-lived stored iterator to reference.

The knowledge gap

The missing concept is:

  • Traits describe capabilities
  • Functions return concrete types unless you explicitly use:
    • impl Trait for one hidden concrete type
    • dyn Trait behind a pointer like Box or &

For iterators, the most common choice is impl Iterator.

Why this matters in real Rust

Returning iterators is extremely common because it lets you:

  • avoid allocating intermediate collections
  • build lazy data pipelines
  • keep APIs flexible and efficient
  • hide complicated concrete iterator types

This is one of Rust's strengths: you can return efficient iterator chains without exposing their full internal type.

Mental Model

Think of a trait like a job description, and a concrete type like an actual worker.

  • Iterator = "someone who can hand out items one at a time"
  • Split = the specific worker doing that job

If a function says it returns an Iterator, that is like saying:

  • "I will send you a worker with these abilities"

But Rust still needs to know how that worker is represented in memory.

There are three common ways to handle that:

  1. Return the exact worker type
    • "I return a Split<'a, char>"
  2. Return impl Iterator
    • "I return some specific worker that implements Iterator, but I won't tell you which one"
  3. Return a boxed trait object
    • "I return any worker that implements Iterator, wrapped in a box"

Your original code tried to return a reference to a temporary worker who disappears as soon as the function ends. That is like handing someone a note saying, "Ask that worker over there," after the worker has already left the building.

Take Quiz

Syntax and Examples

Recommended approach: impl Iterator

This is the most common and cleanest solution.

fn main() {
    let text = "word1 word2 word3";
    println!("{}", to_words(text).take(2).count());
}

fn to_words(text: &str) -> impl Iterator<Item = &str> {
    text.split(' ')
}

Why this works

  • text.split(' ') returns a concrete iterator type.
  • impl Iterator<Item = &str> tells Rust:
    • the function returns some concrete type
    • that type implements Iterator
    • the items are &str

Rust still knows the real type internally, so there is no dynamic dispatch or heap allocation here.

Returning the exact concrete type

You can also return the exact iterator type:

fn to_words(text: &str) -> std::str::Split<'_, char> {
    text.split(' ')
}

This works, but it exposes implementation details. If you later change from split(' ') to something else, the function signature may need to change.

Returning a boxed trait object

Use this when different branches may return different iterator types.

fn to_words(text: &str) -> Box<dyn Iterator<Item = &str> + '_> {
    Box::new(text.split(' '))
}

Why + '_ is there

The iterator borrows from text, so the returned trait object must not outlive text.

Example with filtering

fn non_empty_words(text: &str) -> impl Iterator<Item = &str> {
    text.split(' ').filter(|word| !word.is_empty())
}

fn main() {
    let text = "word1  word2   word3";

    for word in non_empty_words(text) {
        println!("{word}");
    }
}

This shows how useful impl Iterator is: the real return type would otherwise be long and awkward because filter(...) creates another iterator type.

Step by Step Execution

Consider this code:

fn to_words(text: &str) -> impl Iterator<Item = &str> {
    text.split(' ')
}

fn main() {
    let text = "word1 word2 word3";
    let count = to_words(text).take(2).count();
    println!("{count}");
}

Step by step

1. text is created

let text = "word1 word2 word3";

text is a string slice, &str.

2. to_words(text) is called

to_words(text)

Inside to_words:

text.split(' ')

creates a Split iterator that borrows from text.

3. The iterator is returned

The function returns that iterator as impl Iterator<Item = &str>.

This does not collect anything yet. Iterators are lazy.

4. .take(2) wraps the iterator

to_words(text).take(2)

This creates a new iterator that will yield at most 2 items.

Still, nothing has been consumed yet.

5. .count() consumes the iterator

.count()

Now Rust starts pulling values:

  • first item: "word1"
  • second item: "word2"
  • stop because take(2) only allows 2 items

6. The result is printed

println!("{count}");

Output:

2

Important idea

The returned iterator borrows from text, so it can only be used while text is still valid. Rust checks this for you with lifetimes.

Real World Use Cases

Returning iterators is useful in many practical situations.

1. Parsing text input

fn lines_without_empty(text: &str) -> impl Iterator<Item = &str> {
    text.lines().filter(|line| !line.trim().is_empty())
}

Used in:

  • log parsing
  • config file reading
  • command output processing

2. Tokenizing user input

fn tokens(input: &str) -> impl Iterator<Item = &str> {
    input.split_whitespace()
}

Used in:

  • CLI tools
  • search queries
  • simple interpreters

3. Processing API or file data lazily

You may not want to allocate a Vec immediately.

fn csv_fields(line: &str) -> impl Iterator<Item = &str> {
    line.split(',')
}

This is useful when you only need some fields or want to process data one piece at a time.

4. Building reusable data pipelines

fn valid_ids(text: &str) -> impl Iterator<Item = &str> {
    text.split(',').map(str::trim).filter(|s| !s.is_empty())
}

This pattern keeps code modular without forcing allocation.

5. Library APIs

Rust libraries often return iterators so callers can choose what to do next:

  • collect() into a vector
  • count() items
  • find() one item
  • map() into another shape
Take Quiz

Real Codebase Usage

In real projects, developers often return iterators to keep code efficient and expressive.

Common pattern: hide complex iterator types

fn active_usernames(data: &str) -> impl Iterator<Item = &str> {
    data.lines()
        .filter(|line| !line.is_empty())
        .map(|line| line.trim())
}

The actual return type here is a deeply nested iterator type. impl Iterator keeps the API readable.

Common pattern: validation before iteration

Sometimes developers use a guard clause before building the iterator.

fn words_or_empty(text: &str) -> impl Iterator<Item = &str> {
    text.split_whitespace()
}

Or if different logic is needed, they may return a boxed iterator:

fn choose_iter(text: &str, trim: bool) -> Box<dyn Iterator<Item = &str> + '_> {
    if trim {
        Box::new(text.split_whitespace())
    } else {
        Box::new(text.split(' '))
    }
}

Common pattern: chaining in the caller

Functions often return an iterator and let the caller decide how to consume it.

let first_two: Vec<_> = to_words(text).take(2).collect();
let total = to_words(text).count();
let has_admin = to_words(text).any(|w| w == "admin");

This makes utility functions flexible.

Common pattern: prefer borrowed data when possible

Returning &str items avoids extra allocation.

fn tags(line: &str) -> impl Iterator<Item = &str> {
    line.split(',').map(str::trim)
}

This is common in parsers, config readers, and text-processing tools.

When teams avoid boxed iterators

Box<dyn Iterator<...>> is useful, but many codebases prefer impl Iterator when possible because:

  • no heap allocation
  • no dynamic dispatch
  • simpler performance model

Common Mistakes

1. Returning a reference to a temporary iterator

Broken code:

fn to_words(text: &str) -> &dyn Iterator<Item = &str> {
    &(text.split(' '))
}

Why it fails

The iterator created by text.split(' ') is temporary and is dropped when the function ends. A reference to it would be invalid.

Fix

Return the iterator by value:

fn to_words(text: &str) -> impl Iterator<Item = &str> {
    text.split(' ')
}

2. Confusing traits with types

Broken idea:

fn make_iter() -> Iterator<Item = i32> {
    0..10
}

Why it fails

Iterator is a trait, not a concrete sized type.

Fix

Use either:

fn make_iter() -> impl Iterator<Item = i32> {
    0..10
}

or:

fn make_iter() -> Box<dyn Iterator<Item = i32>> {
    Box::new(0..10)
}

3. Forgetting that iterators can borrow

Broken code:

fn to_words() -> impl Iterator<Item = &str> {
    let text = String::from("a b c");
    text.split(' ')
}

Why it fails

The returned iterator would borrow from text, but text is dropped at the end of the function.

Fix

Either accept borrowed input:

fn to_words(text: &str) -> impl Iterator<Item = &str> {
    text.split(' ')
}

Or return owned strings instead.

4. Using split(' ') when split_whitespace() is better

fn to_words(text: &str) -> impl Iterator<Item = &str> {
    text.split(' ')
}

This may produce empty strings when there are multiple spaces.

Often this is better:

fn to_words(text: &str) -> impl Iterator<Item = &str> {
    text.split_whitespace()
}

5. Using impl Iterator when multiple branches return different iterator types

Broken example:

fn choose(text: &str, mode: bool) -> impl Iterator<Item = &str> {
    if mode {
        text.split(' ')
    } else {
        text.lines()
    }
}

Why it fails

impl Iterator still means one concrete type. Both branches must return the same actual type.

Fix

Use a boxed trait object:

fn choose(text: &str, mode: bool) -> Box<dyn Iterator<Item = &str> + '_> {
    if mode {
        Box::new(text.split(' '))
    } else {
        Box::new(text.lines())
    }
}

Comparisons

ApproachExampleBest forProsCons
Concrete typefn f(s: &str) -> std::str::Split<'_, char>Simple cases where you do not mind exposing internalsFast, explicit, no allocationTies API to implementation details
impl Iteratorfn f(s: &str) -> impl Iterator<Item = &str>Most iterator-returning functionsClean API, no heap allocation, no dynamic dispatchMust return one concrete type
Boxed trait objectfn f(s: &str) -> Box<dyn Iterator<Item = &str> + '_>Different branches returning different iterator typesFlexible, hides type completelyHeap allocation and dynamic dispatch
Reference to trait objectfn f(...) -> &dyn Iterator<Item = ...>Rare cases where iterator is stored elsewhereCan avoid ownership transferHard to use correctly; cannot reference temporaries

impl Trait vs dyn Trait

Featureimpl Iteratordyn Iterator
DispatchStaticDynamic
Heap allocation requiredNoUsually yes, with Box
Can hide concrete typeYesYes
Can return different concrete types from branchesNoYes
PerformanceUsually betterSlight overhead

split(' ') vs split_whitespace()

MethodBehavior
split(' ')Splits only on the space character and may produce empty items
split_whitespace()Splits on any whitespace and skips repeated whitespace cleanly

For word parsing, split_whitespace() is often the better choice.

Take Quiz

Cheat Sheet

Quick rules

  • Iterator is a trait, not a concrete type.
  • Return iterators by value, not by reference to a temporary.
  • Use impl Iterator<Item = T> for most cases.
  • Use Box<dyn Iterator<Item = T> + 'a> when different branches return different iterator types.
  • If the iterator borrows from input, the returned iterator must carry that lifetime.

Common patterns

Return an iterator borrowing from input

fn words(text: &str) -> impl Iterator<Item = &str> {
    text.split_whitespace()
}

Return exact iterator type

fn words(text: &str) -> std::str::SplitWhitespace<'_> {
    text.split_whitespace()
}

Return boxed trait object

fn words(text: &str) -> Box<dyn Iterator<Item = &str> + '_> {
    Box::new(text.split_whitespace())
}

Lifetime reminder

If items come from text, the iterator usually borrows from text.

That means this is fine:

fn words(text: &str) -> impl Iterator<Item = &str> {
    text.split_whitespace()
}

But this is not:

fn words() -> impl Iterator<Item = &str> {
    let s = String::from("a b c");
    s.split_whitespace()
}

Good default answer for this question

fn to_words(text: &str) -> impl Iterator<Item = &str> {
    text.split_whitespace()
}

FAQ

Why can't I return Iterator directly in Rust?

Because Iterator is a trait, not a concrete sized type. Rust needs either a concrete type, impl Iterator, or a trait object like Box<dyn Iterator<...>>.

Should I use impl Iterator or Box<dyn Iterator>?

Use impl Iterator by default. Use Box<dyn Iterator> when you need to return different concrete iterator types from different branches.

Why did returning &Iterator fail?

You were returning a reference to a temporary iterator created inside the function. That temporary is dropped when the function ends.

Does impl Iterator allocate memory on the heap?

No. impl Iterator uses static dispatch and does not require heap allocation by itself.

What lifetime does the returned iterator have?

If the iterator borrows from text, its lifetime is tied to text. Rust usually infers this automatically.

Is split(' ') the same as split_whitespace()?

No. split(' ') splits only on the space character and can return empty strings. split_whitespace() handles repeated spaces and other whitespace more cleanly.

Can I return different iterators with impl Iterator?

Not unless they are the same concrete type. If branches produce different iterator types, use Box<dyn Iterator<...>> or refactor the logic.

Take Quiz

Mini Project

Description

Build a small Rust utility that extracts meaningful words from a line of text and lets the caller continue chaining iterator methods like take, filter, and count. This demonstrates how to return an iterator from a function in a practical text-processing scenario.

Goal

Create a function that returns an iterator over cleaned words from a string, then use it to count and collect selected words.

Requirements

  • Write a function that accepts &str input and returns an iterator.
  • Ignore empty entries and surrounding whitespace.
  • Convert words to lowercase before returning them.
  • In main, print the first three processed words.
  • In main, also print how many processed words exist in total.
Take Quiz

Keep learning

Related questions

View more