---
title: 'The Shorthand for Error Propagation'
source: 'https://academia.sh/en/courses/rust-abstraction/the-question-mark-operator'
course: 'Traits, Generics and Error Handling'
language: en
updated: '2026-09-15T04:58:42+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Shows by executing that the body after the question mark runs once on a successful resolve and never on a failed one'
    - 'Verifies by comparison that the shorthand gives the same counts as the hand-written branch'
    - 'Distinguishes with an example that early return silently skips the remaining body, and that the skipped step produces no side effect'
---

# The Shorthand for Error Propagation

Measuring that the shorthand which reduces checking an error and returning early to a single mark gives exactly the same counts as the same job written by hand with a branch — that it shortens the source without changing the run.

The Failure as a Return Value lesson showed that when the same function is called
repeatedly, the success and failure arms run independently of each other, and both
repeat the same three-line pattern inside the same `match` body: parse the value, report
if there is an error, otherwise continue. If a function does more than one parse, this
pattern repeats in the same way each time — the same two-armed `match` after every parse.
This lesson measures the shorthand that reduces this repetition to a single mark: the
question mark (`?`).

As this topic's third lesson, this lesson now assumes that success and failure are
separate bodies — that distinction was established in the previous lesson. The question
asked here goes one step further: how much can the `match` skeleton that separates these
two bodies be shortened in the source, and does that shortening change the counts in the
run? The answer emerges by comparing the two runs below.

The Interfaces, Errors and Generics course established that an error is carried as a
return value and does not leave the normal control flow. The question mark does not
break that decision either — it still produces a return value, still uses an ordinary
function return — it only shortens how long it takes to write that return in the source.
This lesson does not return to the *where* question that course measured; what it
measures is whether the shorthand changes the run.

The Functions and Expression Orientation lesson measured when a function needs to return
early before its body finishes: if the rest of the body will not be computed, an
explicit `return` made that fact visible in the source. The question mark uses exactly
this early return — only instead of writing the `return` form by hand, it places a
single mark right after a `Result` call. This lesson does not re-establish the
early-return concept that lesson set up; it only measures whether this particular
shorthand gives *the same counts* as a hand-written early return.

## Same Counts, Shorter Source

The question mark is written right after a call that returns a `Result`, inside a
function that itself returns a `Result`. If the call returns `Ok`, the value inside it is
unwrapped and the body continues from where it left off; if it returns `Err`, the
function returns immediately with its own `Err`, and the rest of the body never runs.

This shortcut has one condition: the question mark can only be written when the function
it is inside also returns a `Result` (or `Option`). If `main`'s return type is `()`, `?`
cannot be placed directly after a call that produces a `Result` inside `main`'s body —
there is no `Result` slot for the early return to return into. This lesson does not
violate that condition: `resolve` carries its own `Result` return type, and `?` is used
only inside its body; the calls inside `main`, meanwhile, bind the returned `Result` to a
variable and print it with `{:?}`, without using `?` themselves.

In the `resolve` function below, `text.parse()` returns a `Result<i32, ParseIntError>`,
but the function's own return type is `Result<i32, NumberError>` — two different error
types. The question mark resolves this difference on its own: thanks to the
`impl From<ParseIntError>` implementation written for `NumberError`, the error `parse`
produces is automatically converted into `NumberError`. This conversion itself is the
subject of the Custom Error Types lesson; in this lesson, only the fact that `?` *uses*
this conversion is visible, and it does not affect the count measured in this lesson.

```rust
// with_question.rs — the resolve slice of times.rs: the body after the question runs once on success, never on failure
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;

type Ledger = Rc<RefCell<BTreeMap<String, u32>>>;

fn record(ledger: &Ledger, name: &str) {
    *ledger.borrow_mut().entry(name.to_string()).or_insert(0) += 1;
}
fn times(ledger: &Ledger, name: &str) -> u32 {
    *ledger.borrow().get(name).unwrap_or(&0)
}

#[derive(Debug)]
struct NumberError;

impl From<std::num::ParseIntError> for NumberError {
    fn from(_: std::num::ParseIntError) -> NumberError {
        NumberError
    }
}

fn resolve(ledger: &Ledger, text: &str, label: &str) -> Result<i32, NumberError> {
    let number: i32 = text.parse()?;
    record(ledger, label);
    Ok(number * 2)
}

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(BTreeMap::new()));

    let success = resolve(&ledger, "21", "after-question-success");
    let failure = resolve(&ledger, "twenty-one", "after-question-failure");

    println!("success call: {:?}", success);
    println!("failure call: {:?}", failure);
    println!();
    for name in ["after-question-success", "after-question-failure"] {
        println!("{:<24}{} times", name, times(&ledger, name));
    }
}
```

```
success call: Ok(42)
failure call: Err(NumberError)

after-question-success  1 times
after-question-failure  0 times
```

**EH7.** In the `resolve(&ledger, "21", ...)` call, `text.parse()` successfully produces
`21`; the question mark unwraps this value, binds it to `number`, and the body continues
from where it left off — the `record` call runs, and the `after-question-success` line
increases by one. In the `resolve(&ledger, "twenty-one", ...)` call, by contrast, `parse`
produces an error; the question mark converts this error into `NumberError` and returns
from the function immediately. The `record` call never gets its turn —
`after-question-failure` stays at zero. This is the same behavior as the `Ok`/`Err` arms
in the previous lesson: one runs, the other does not. The difference is that these two
arms are no longer written in a separate `match`, but are now hidden behind a single
mark.

Looking at the `resolve` function's return type, only one line in its body
(`let number: i32 = text.parse()?;`) looks related to `Result`; the remaining two lines
(the `record` call and `Ok(number * 2)`) read like ordinary code. This shows how small a
trace the question mark leaves in the source — no matter how many separate parses a
function does, it completes the error check by adding a single character after each one.
The `label` parameter carrying the name passed to `record` lets this lesson track two
different calls (success and failure) through the same function but with separate
counter keys; in real usage this parameter would not be present, it was added only for
measurement.

The same function can also be built with a hand-written branch. The run below replaces
the question mark with a `match` and an early `return`, and is called with the same two
inputs.

```rust
// manual_branch.rs — the same job written with a hand-written branch gives the same counts
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;

type Ledger = Rc<RefCell<BTreeMap<String, u32>>>;

fn record(ledger: &Ledger, name: &str) {
    *ledger.borrow_mut().entry(name.to_string()).or_insert(0) += 1;
}
fn times(ledger: &Ledger, name: &str) -> u32 {
    *ledger.borrow().get(name).unwrap_or(&0)
}

#[derive(Debug)]
struct NumberError;

fn resolve_by_hand(ledger: &Ledger, text: &str, label: &str) -> Result<i32, NumberError> {
    let parsed = text.parse::<i32>();
    let number = match parsed {
        Ok(value) => value,
        Err(_) => return Err(NumberError),
    };
    record(ledger, label);
    Ok(number * 2)
}

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(BTreeMap::new()));

    let success = resolve_by_hand(&ledger, "21", "after-question-success");
    let failure = resolve_by_hand(&ledger, "twenty-one", "after-question-failure");

    println!("success call: {:?}", success);
    println!("failure call: {:?}", failure);
    println!();
    for name in ["after-question-success", "after-question-failure"] {
        println!("{:<24}{} times", name, times(&ledger, name));
    }
}
```

```
success call: Ok(42)
failure call: Err(NumberError)

after-question-success  1 times
after-question-failure  0 times
```

**EH8.** The two runs give exactly the same counts: `after-question-success` one,
`after-question-failure` zero. The source differs — one line (`text.parse()?`) in the
first, a five-line `match` in the second — but the run is the same. The question mark
does not add new behavior; it does the same job as the compiler-generated code of a
hand-written `match` and early `return`, by writing less. This is direct proof that the
shorthand shortens the *source*, not the *run*: the two programs have source of
different length, but produce the same binary behavior.

The `match` in `resolve_by_hand` differs from the `match` in the previous lesson in one
respect: the `Err` arm does not produce a value and hand it off to the rest of the body,
it exits the function directly with `return Err(NumberError)`. This does not contradict
the principle established in the Control Flow lesson that `match` is an expression —
`return` is not an expression but a statement, and a statement can be written inside a
`match` arm; the "value" that arm produces never reaches the rest of the body, because
the function itself is exited. The `Ok` arm, on the other hand, produces an ordinary
value (`value`), and that value is bound to `number`, and the body continues from there.
The two arms' different natures — one exits the function, the other yields a value —
remain exactly the same behind the question mark too, just invisible.

The real payoff of the shortening in the source shows up when a function does more than
one parse. A function that parses three separate fields in turn would need a five-line
`match`, repeated three times by hand (fifteen lines in total); the same function drops
to three lines with the question mark — a single mark after each field. The two runs
measured in this lesson (a one-line shorthand, a five-line hand-written form) are the
smallest example of this difference; as the number of fields grows, the difference in the
source grows too, but the `after-question-success` / `after-question-failure` pair both
forms produce stays the same. The shorthand's growth is not linear — while the number of
source lines drops, the run itself never changes.

The question mark works the same way on `Option` too: if the function it is inside
returns `Option`, it unwraps the value inside `Some`, and in the `None` case it returns
`None` from the function immediately. This lesson does not exercise that second
use — the `Option` methods measured in the Expressing Absence with a Type lesson were
built around producing a value (`unwrap_or`, `unwrap_or_else`), while what is measured
here is early return. Even though both uses carry the same mark, one produces a value
(the way `unwrap_or_else` does), the other exits the function; what this lesson measures
is only its form on `Result`, the one that exits the function.

## A Limit on the Measurement

In the run above, `after-question-success` and `after-question-failure` always move
together: when one is one, the other is zero, and when one is zero, the other is one.
This is not the result of a single observation, but of the state of the same line across
two different calls — both actually represent the same `record(ledger, label)` call, only
the `label` argument changes with the caller.

**EH9.** Early return silently skips the body that follows it — it prints no warning,
leaves no mark. In the call made with the `"twenty-one"` input, the `record` call never
runs, so no record is dropped into the counter ledger; this leaves no trace saying "a
step was skipped," it only shows up through the *absence* of the expected record. If the
skipped step itself had produced a side effect — writing to a file, incrementing a
counter, printing a log line — that side effect would not have happened either. This
lesson shows this exactly through the `record` call itself: `record` is the skipped
step's *only* side effect, and because that side effect does not happen, the count stays
at zero.

This is a different situation from the previous lesson's `Err` arm being written in the
source but not run. There, both arms stood explicitly inside the same `match`, and both
were visible. Here, the "skipped body" is not written as a separate arm — every line
after the question mark is implicitly skipped in the error case. Someone reading the
source can see under what condition the lines after `?` will run, but does not see in the
source that the compiler produces a separate arm for this; that arm only surfaces in the
run, when measured with a counter.

This silence is the result of a design decision, not a flaw. The question mark prints no
warning, because the error itself is already carried as a return value — as long as the
caller checks that value, the "why it was skipped" information is not lost, it just lives
in the return value rather than in the rest of the body. This lesson does not track that
information; it only counts whether the rest of the body ran. If the code calling
`resolve` does not check the returned `Result`, it loses both that information and the
chance to see what was skipped — but that is the result of the caller's decision not to
check, not of the question mark.

The same silent skipping repeats in the same way through a chain of question marks that
crosses function boundaries in succession. If a function calls another function
internally with a question mark, and that function in turn calls a third function with a
question mark, the failure at the bottom rises all the way to the top by silently
skipping all three bodies — none of them leaving a trace. This lesson does not exercise
this multi-layered chain; it only shows, at a single boundary, that the skipping itself
is silent.

Thinking through this three-layer chain makes the limit even clearer. Say the bottom
function does a parse and fails; the middle layer calling it would have run three more
lines in its own body, but never runs them; the top caller would have run five more
lines, and it does not run them either. In total, an eight-line body is silently skipped
because of a single failure — but nothing in the source, no comment, no special syntax,
marks these eight lines as "skippable." Only the fact that all three layers return
`Result` and that each layer's call is followed by a question mark shows that this
silent skipping is possible.

The distinction measured in this topic's first two lessons still holds here: the
difference between eager and lazy bodies in `Option`, and between the success and
failure arms in `Result`, stands in the same way here too — only now the `match` arms
are no longer written by hand, they are hidden behind a single mark. The shorthand does
not eliminate any of the three classes measured (every time, only when needed, never); it
only shrinks the source that carries those classes.

## Summary

- The question mark reduces the early return written after a `Result` call to a single
  mark; if it returns `Ok`, it unwraps the value and the body continues, if it returns
  `Err`, the function returns immediately.
- The body after the question mark runs **only when needed**, when the call
  succeeds — it runs **never** when the call fails.
- The same function, built with a hand-written `match` and an early `return`, gives **the
  same counts**: the shorthand shortens the source, not the run — and this difference
  grows as a function does more than one parse.
- Early return silently skips the remaining body; the skipped step's side effect does not
  happen at all either — no warning, no partial record.
- The skipped body does not appear as a separate arm in the source; it is noticed only
  through the missing record, when measured with a counter.
- The question mark automatically converts different error types with `From`; who calls
  this conversion is the subject of the next lesson.

## Next Step

The `NumberError` above carried a `From` implementation converting
`std::num::ParseIntError` into its own type — how the question mark calls this
conversion was not shown in this lesson, only used. There is no explicit line in the
`resolve` function's body calling this conversion; the `text.parse()?` form itself both
checks the error and converts it when needed. The Custom Error Types lesson measures who
calls this conversion: does the shorthand make this call without the caller writing
anything, or is a hand-written conversion needed — and can both approaches carry the
same information, or does the implicit conversion lose something?
