Skip to content

← Errors Are Values step 8 of 24

Easy Primitives

Result combinators and map_err

Parse a list of strings as i32, double each one, and produce one output string per input — naming the offending input when parsing fails.

pub fn parse_all(raw: Vec<String>) -> Vec<String>
  • "20""40"
  • "12x""bad input: 12x"
  • """bad input: " (the original string, empty, after the colon and space)

The whitespace rule is the one that catches people: str::parse trims for you, so " 5 " parses to 5 — but the error message quotes the original string, whitespace included: "bad input: x ".

Why this problem exists

std‘s parse error is ParseIntError, and its Display is exactly one of:

invalid digit found in string
cannot parse integer from empty string
number too large to fit in target type

Read those again. Not one of them tells you which string failed. That is not an oversight — ParseIntError is produced deep inside std, which does not know what your input was called, where it came from, or whether it is worth mentioning. You know that. Attaching it is your job, and the tool for the job is map_err.

s.trim().parse::<i32>()                       // Result<i32, ParseIntError>
    .map_err(|_| format!("bad input: {s}"))   // Result<i32, String>

map_err transforms the Err payload and leaves Ok untouched — the mirror image of map. After ? itself, it is the most-used error-handling method in real Rust, because every time you cross a boundary between two libraries you have somebody else’s error type in hand and need your own.

The starter is blocked by a lint you should read carefully

error: manual implementation of `ok`
   |
   |             let parsed = match s.trim().parse::<i32>() {
   |                          ^ help: replace with: `s.trim().parse::<i32>().ok()`

clippy::manual_ok_err is on by default, and the obvious match therefore does not survive the gate. But look at what the suggestion is: .ok() throws the error away. The lint is telling you that if all you wanted was to discard the error, there is a one-word way to say so — which makes it very obvious that discarding is what you were doing, and here that is the bug.

The vocabulary

Transformmap(f) on the Ok, map_err(f) on the Err, and_then(f) when your f is itself fallible, or_else(f) to recover.

Extractunwrap_or(v), unwrap_or_else(f), unwrap_or_default(), map_or(default, f), map_or_else(err_f, ok_f).

Convert to Option.ok() keeps the value and drops the error; .err() keeps the error and drops the value.

Askis_ok(), is_err(), is_ok_and(f), is_err_and(f).

Two asymmetries worth memorising, because the error messages when you get them wrong are confusing:

  • unwrap() needs E: Debug (it has to print the error it is panicking on), while unwrap_err() needs T: Debug. E0277 with a Debug is not implemented note usually means you called one of these on a type you never derived Debug for.
  • Result::flatten exists but is only stable since 1.89. If you are targeting anything older, and_then(|x| x) is the portable form.

The honest tension about map_err(|_| …)

Clippy has a restriction lint, map_err_ignore, that objects to map_err(|_| …) on the grounds that you destroyed the underlying cause. It is allow-by-default and it is not enabled here, but the objection is real:

  • At a boundary — a CLI printing a message to a human, an HTTP handler producing a 400 — discarding the source is usually right. Nobody wants invalid digit found in string in a form validation message.
  • In a library, discarding is usually wrong. Your caller may want to distinguish “not a number” from “too large”, and once you have flattened it to a String they cannot.

Later in this track you will keep both: a custom error type that carries the original as its source(), so the message is yours and the cause survives.

Notes

  • map_unwrap_or (pedantic) is the lint that pushes r.map(f).unwrap_or(d) towards r.map_or(d, f). Same meaning, one fewer intermediate.
  • unnecessary_literal_unwrap fires on Ok::<i32, ()>(3).unwrap() — unwrapping something you just built. It shows up in test code more than anywhere else.
  • 3.5 and 9999999999 both fail to parse as i32 — one is a float, one overflows — and both produce a ParseIntError. Another reason the caller wants the input echoed back.

Remember the grade is compile + tests + clippy -D warnings.