Question

Handling Errors While Mapping Iterators in Rust

rustrust-result

Question

I have a Rust function that returns a Result:

fn find(id: &Id) -> Result<Item, ItemError> {
    // ...
}

Then I use it like this:

let parent_items: Vec<Item> = parent_ids
    .iter()
    .map(|id| find(id).unwrap())
    .collect();

How should I handle the case where one of the map iterations fails?

I know I could use flat_map, and in this case any errors would effectively be ignored:

let parent_items: Vec<Item> = parent_ids
    .iter()
    .flat_map(|id| find(id).into_iter())
    .collect();

Because a Result iterator yields either 0 or 1 items depending on whether it is Err or Ok, flat_map would skip the error case.

However, I do not want to ignore errors. I want the whole operation to stop on the first failure and return a new error, or propagate the existing error.

What is the idiomatic way to do this in Rust?

Short Answer

By the end of this page, you will understand how to transform an iterator of Result values into a single Result in Rust, how collect() can stop on the first error, and when to use patterns like ?, map, and try_for_each for clean error handling.

Concept

In Rust, this pattern is about collecting fallible operations.

When you call a function like:

find(id) -> Result<Item, ItemError>

inside an iterator, each iteration produces a Result<Item, ItemError> instead of a plain Item.

That means this:

parent_ids.iter().map(find_like_function)

creates an iterator of this shape:

Iterator<Item = Result<Item, ItemError>>

The important idea is that Rust can collect this into:

Result<Vec<Item>, ItemError>

This is idiomatic Rust.

collect() knows how to do this because Result implements the necessary traits. It will:

  • keep collecting Ok(...) values into the Vec
  • stop at the first Err(...)
  • return that error immediately

This matters because many real programs perform repeated operations that can fail:

  • loading records from IDs
  • parsing multiple inputs
  • validating request fields
  • reading files
  • calling external services

Rust encourages you to represent those failures explicitly instead of hiding them or panicking with unwrap().

So instead of forcing success with unwrap(), you usually let the iterator produce Results and then collect them into one final Result.

Mental Model

Think of an iterator of Results like a conveyor belt of boxes.

  • An Ok(item) box contains a real value.
  • An Err(error) box contains a failure notice.

collect::<Result<Vec<_>, _>>() is like a worker loading items into a truck:

  • if the next box contains an item, it gets loaded
  • if the next box contains an error, loading stops immediately
  • the worker reports the error instead of continuing

So the process is all-or-nothing:

  • either you get a full Vec<Item>
  • or you get the first error that occurred

That is why collect() is often the cleanest answer for this pattern in Rust.

Take Quiz

Syntax and Examples

Core syntax

If your iterator produces Result<T, E>, you can collect it into Result<Vec<T>, E>:

let results: Result<Vec<T>, E> = items
    .iter()
    .map(|item| fallible_operation(item))
    .collect();

Example using your case

fn load_parent_items(parent_ids: &[Id]) -> Result<Vec<Item>, ItemError> {
    parent_ids
        .iter()
        .map(|id| find(id))
        .collect()
}

This works because:

  • map(|id| find(id)) produces Result<Item, ItemError> for each id
  • collect() turns that into Result<Vec<Item>, ItemError>

Propagating the error with ?

Usually you write it like this:

fn load_parent_items(parent_ids: &[Id]) -> Result<Vec<Item>, ItemError> {
    let parent_items: Vec<Item> = parent_ids
        .iter()
        .map(|id| find(id))
        .collect::<Result<Vec<_>, _>>()?;

    Ok(parent_items)
}

The ? operator means:

  • if collection succeeded, extract the Vec<Item>
  • if it failed, return the Err from the current function

Mapping to a new error type

If you want to return a different error type, use map_err:

fn load_parent_items(parent_ids: &[Id]) -> Result<Vec<Item>, MyError> {
    parent_ids
        .iter()
        .map(|id| find(id).map_err(MyError::ItemLookupFailed))
        .collect()
}

Here each ItemError is converted into MyError before collection.

Step by Step Execution

Consider this code:

fn parse_numbers(inputs: &[&str]) -> Result<Vec<i32>, std::num::ParseIntError> {
    inputs
        .iter()
        .map(|s| s.parse::<i32>())
        .collect()
}

Now imagine:

let inputs = ["10", "20", "abc", "40"];

What happens step by step

  1. inputs.iter() creates an iterator over the strings.
  2. .map(|s| s.parse::<i32>()) transforms each string into a Result<i32, ParseIntError>.
  3. The iterator now conceptually yields:
Ok(10)
Ok(20)
Err(...)
Ok(40)
  1. collect() starts building a Vec<i32>.
  2. It sees Ok(10) and stores 10.
  3. It sees Ok(20) and stores 20.
  4. It sees Err(...) and stops immediately.
  5. It returns that error.
  6. The final Ok(40) is never processed.

Result

Err(parse_error)

This short-circuiting behavior is exactly what you want when failure should stop the whole operation.

Real World Use Cases

Loading records from a database

You may have a list of IDs and need to load each corresponding record:

let users: Result<Vec<User>, DbError> = user_ids.iter().map(fetch_user).collect();

If one record fails to load, the whole operation returns an error.

Parsing user input

When converting many strings into numbers or dates, you often want all values to be valid:

let values: Result<Vec<i32>, _> = raw_values.iter().map(|s| s.parse()).collect();

Validating configuration

Applications often read multiple config fields and stop on the first invalid value.

Building API request objects

If each field transformation can fail, collecting Results keeps the code concise and safe.

File processing pipelines

When reading and transforming lines from a file, you may want to abort on the first malformed line instead of silently skipping it.

Take Quiz

Real Codebase Usage

In real Rust codebases, developers commonly use this pattern in a few standard ways.

1. collect() for fallible mapping

This is the most common pattern when you want to transform many values and keep them all only if every step succeeds.

let items: Result<Vec<_>, _> = ids.iter().map(find).collect();

2. ? for early return

Functions that return Result often combine collect() with ?:

fn build(ids: &[Id]) -> Result<Vec<Item>, ItemError> {
    let items = ids.iter().map(find).collect::<Result<Vec<_>, _>>()?;
    Ok(items)
}

This avoids manual match blocks.

3. map_err for domain-specific errors

Real projects often have one top-level error type:

let items = ids
    .iter()
    .map(|id| find(id).map_err(AppError::from))
    .collect::<Result<Vec<_>, _>>()?;

4. try_for_each when you do not need a collected Vec

If you only care about side effects and want to stop on error:

ids.iter().try_for_each(|id| {
    let item = find(id)?;
    println!("{:?}", item);
    Ok::<(), ItemError>(())
})?;

5. Guarding against unwrap() in production code

In real codebases, unwrap() is usually avoided unless failure is impossible or truly unrecoverable. For recoverable operations, returning Result is preferred.

Common Mistakes

Using unwrap() on fallible operations

Broken code:

let parent_items: Vec<Item> = parent_ids
    .iter()
    .map(|id| find(id).unwrap())
    .collect();

Problem:

  • the program panics on error
  • callers cannot handle the failure

Better:

let parent_items: Result<Vec<Item>, ItemError> = parent_ids
    .iter()
    .map(|id| find(id))
    .collect();

Using flat_map and accidentally dropping errors

Broken idea:

let parent_items: Vec<Item> = parent_ids
    .iter()
    .flat_map(|id| find(id).into_iter())
    .collect();

Problem:

  • Err values disappear
  • failures are silently ignored

Use this only when skipping errors is truly intended.

Expecting collect() to produce Vec<Result<T, E>>

This:

let x = parent_ids.iter().map(|id| find(id)).collect::<Vec<_>>();

produces:

Vec<Result<Item, ItemError>>

That means errors are still inside the vector. If you want short-circuiting behavior, collect into Result<Vec<_>, _> instead.

Forgetting the function must return Result

If you use ?, your surrounding function must also return a compatible Result.

Broken code:

fn load(ids: &[Id]) -> Vec<Item> {
    ids.iter().map(find).collect::<Result<Vec<_>, _>>()?
}

Fix:

fn load(ids: &[Id]) -> Result<Vec<Item>, ItemError> {
    ids.iter().map(find).collect()
}

Not converting error types when needed

If your function returns a different error type, convert it with map_err or From.

.map(|id| find(id).map_err(AppError::from))

Comparisons

Common approaches

ApproachWhat it returnsStops on error?Keeps errors?Typical use
map(...).collect::<Result<Vec<_>, _>>()Result<Vec<T>, E>YesYesBest for collecting all successful values or returning first error
map(...).collect::<Vec<_>>()Vec<Result<T, E>>NoYesUse when you want to inspect each result later
`flat_map(xresult.into_iter()).collect::<Vec<_>>()`Vec<T>No
try_for_each(...)Result<(), E>YesYesBest when you do not need to build a collection
manual for loop with ?Result<Vec<T>, E>YesYesGood when logic is more complex

collect() vs manual loop

A manual loop is sometimes clearer if you need extra logic:

fn load_parent_items(parent_ids: &[Id]) -> Result<Vec<Item>, ItemError> {
    let mut items = Vec::new();

    for id in parent_ids {
        let item = find(id)?;
        items.push(item);
    }

    Ok(items)
}

This is equivalent in behavior to the iterator version, but easier to extend with logging, conditions, or extra processing.

collect() vs try_for_each

  • Use collect() when you want a resulting collection.
  • Use try_for_each() when you only want to perform actions and stop on failure.
Take Quiz

Cheat Sheet

// Fallible mapping into a Vec
let items: Result<Vec<_>, _> = ids.iter().map(|id| find(id)).collect();

// Propagate error
let items = ids.iter().map(|id| find(id)).collect::<Result<Vec<_>, _>>()?;

// Convert error type first
let items = ids
    .iter()
    .map(|id| find(id).map_err(AppError::from))
    .collect::<Result<Vec<_>, _>>()?;

// Side effects only
ids.iter().try_for_each(|id| {
    let item = find(id)?;
    println!("{:?}", item);
    Ok::<(), ItemError>(())
})?;

Rules to remember

  • map() does not handle errors by itself.
  • unwrap() panics; it does not return an error.
  • collect::<Result<Vec<_>, _>>() stops on the first Err.
  • flat_map(...into_iter()) on Result drops errors.
  • Use ? to return errors early from the surrounding function.
  • If needed, convert errors with map_err.

Type pattern

Iterator<Item = Result<T, E>>
    -> collect()
Result<Vec<T>, E>

FAQ

How do I stop iteration on the first error in Rust?

Use an iterator that produces Result<T, E> and collect it into Result<Vec<T>, E>:

let items: Result<Vec<_>, _> = ids.iter().map(find).collect();

collect() stops at the first Err.

Does map() itself stop on Err?

No. map() only transforms items. The short-circuiting happens when you use consumers like collect() into a Result, or methods like try_for_each().

Why is unwrap() a bad choice here?

Because unwrap() panics if an error occurs. That crashes the current flow instead of letting you return a controlled error to the caller.

Can I return a different error type?

Yes. Use map_err() inside the iterator:

.map(|id| find(id).map_err(AppError::LookupFailed))

When should I use try_for_each() instead of collect()?

Use try_for_each() when you do not need a Vec of results and only want to perform work until an error occurs.

What if I want to keep all errors instead of stopping at the first one?

Then collect::<Result<Vec<_>, _>>() is not the right tool. You would usually collect into Vec<Result<T, E>> and process the errors afterward.

Is a manual for loop less idiomatic than collect()?

No. Both are idiomatic. Use collect() for concise transformations and a for loop when the logic is easier to read that way.

Take Quiz

Mini Project

Description

Build a small Rust function that loads product prices from a list of string inputs. Each string should be parsed into a number. If any input is invalid, the function should stop immediately and return the parsing error. This demonstrates how collect() can turn an iterator of Result values into a single Result<Vec<_>, _>.

Goal

Create a function that converts a list of strings into a Vec<i32> and returns an error if any string cannot be parsed.

Requirements

  • Write a function that accepts a slice of string slices (&[&str]).
  • Parse each string into an i32.
  • Return Result<Vec<i32>, std::num::ParseIntError>.
  • Stop on the first invalid number.
  • Show at least one successful call and one failing call.
Take Quiz

Keep learning

Related questions

View more