Skip to content

← Errors Are Values step 11 of 24

Easy Primitives

Propagating with `?`

Run an input through four fallible steps, stopping at the first failure.

pub fn pipeline(input: String) -> Result<i32, String>

The four helpers are already written for you and must not be changed:

fn non_empty(s: &str) -> Result<&str, String>   // "empty input"
fn strip_tag(s: &str) -> Result<&str, String>   // "missing `n=` prefix in `42`"
fn to_number(s: &str) -> Result<i32, String>    // "not a number: forty"
fn in_range(n: i32) -> Result<i32, String>      // "out of range: 5000"

pipeline trims the input, then feeds it through all four in order. If any step fails, its error comes straight back out unchanged. "n=42" gives Ok(42); "n=5000" gives Err("out of range: 5000").

Your body should be four lines.

The starter is correct and unacceptable

It passes every test. It is also twenty lines of the same three-line incantation, and clippy will tell you so four times:

error: this `match` expression can be replaced with `?`
   |
   |     let trimmed = match non_empty(input.trim()) {
   |                   ^ help: try instead: `non_empty(input.trim())?`

clippy::question_mark is on by default and it recognises exactly this shape: match r { Ok(v) => v, Err(e) => return Err(e) }. Which is worth pausing on, because it tells you what ? is. ? is not exception handling. It is not a throw. It is that match, spelled with one character:

expr? unwraps the Ok and gives you the value, or returns Err from the enclosing function — after passing the error through From::from.

The From::from part does nothing here (every step already returns Result<_, String>, and From<String> for String is the identity) but it is the entire mechanism behind everything later in this track. Keep it in mind.

And the gate closes in the other direction too

Having discovered ?, the natural overcorrection is to use it on the last step as well:

Ok(in_range(n)?)     // don't
error: enclosing `Ok` and `?` operator are unneeded
help: remove the enclosing `Ok` and `?` operator

clippy::needless_question_mark is also on by default. Unwrapping a value out of a Result and immediately putting it back in the same kind of Result is a round trip that does nothing. The last step already has the right type — just return it.

So both directions are gated: a match pyramid fails, and a ?-happy solution fails. What survives is the version where each ? is doing real work.

Reading a ? chain

let trimmed = non_empty(input.trim())?;
let body = strip_tag(trimmed)?;
let n = to_number(body)?;
in_range(n)

Four lines, four exit points, and the happy path reads top to bottom with no error handling in the way. That is the trade ? makes: it hides the failure edges so the success path is legible. When you need the edges back — to add context, or to recover — you go back to match, or to map_err.

One nuance that costs people an afternoon: ? returns from the enclosing function or closure, not from the enclosing block. Putting a ? inside a closure returns from that closure, which is a different thing from returning from the function around it. That is exactly why try { … } blocks are a separate (and still unstable) feature.

Notes

  • try_err is a restriction lint that fires on Err(e)? — a way of writing return Err(e.into()) that people discover and then overuse. Prefer the plain return Err(...).
  • Result<_, String> here is scaffolding, and you should not copy it. A String error cannot be matched on, cannot carry structured data, and allocates on every failure. Items 6.14 and 6.15 replace it with a real error type; this problem uses String only so that ? needs no conversions yet.
  • ? is rejected in a const fn (E0658) — the Try trait itself is still unstable, so const support has nowhere to hang.

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