---
title: 'Failure as a Return Value'
source: 'https://academia.sh/en/courses/rust-abstraction/result'
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 success and failure arms are separate bodies'
    - 'Verifies by counting how the arm counts diverge while the total call count stays fixed'
    - 'Distinguishes with an example that an arm written in the source may never run in a given run'
---

# Failure as a Return Value

Writing an operation's success and failure paths as separate bodies in the source, and measuring how the run counts of these two arms diverge while the total call count stays fixed as the same function is called repeatedly.

`None` has no reason — it says that a value was not found, but not why. When a file-open
call fails, what the caller needs to know is not just "no result"; it also needs to know
whether the file was not found, permission was denied, or something else happened. This
lesson measures how many times, and which arm, a result that also carries a
reason — `Result<T, E>` — runs.

The axis measured in this topic's first lesson (whether it runs eagerly or lazily) does
not hold here; there is no relationship like "one is the cheaper form of the other"
between `Ok` and `Err`. Instead, a new question comes into play: of two different bodies
sitting side by side in the source, which one runs depends on the input — and how does
that vary, and accumulate, over the course of a run?

The Interfaces, Errors and Generics course established that an error is placed in the
third of its three binding kinds — *inside the value*: the error is carried neither in a
signature nor at a call site, but directly in the return value itself. This lesson
inherits that decision — `Result` follows exactly this arrangement — but asks a different
question: when the same function is called repeatedly, how many times does each of the
success and failure arms run?

The Expressing Absence with a Type lesson measured that the eager and lazy forms do *the
same job*, differing only in how many times they run. The difference measured in this
lesson sits somewhere else: the `Ok` and `Err` arms do not do the same job — they run two
entirely different bodies. The question is no longer "how many times does the same job
run" but "which job, how often" — what share of the run belongs to each of the two
different bodies written in the source.

## A Result, Two Paths

`Result<T, E>` is another standard-library instance of the enum mechanism set up in the
previous course: `Ok(T)` carries the successful result, `Err(E)` carries the reason for
failure. Its difference from `Option` is that, unlike `None`, `Err` carries a value of
its own — failure here is not an empty state, but data with its own type. `Option`
answers the question "is there a value"; `Result` can also answer "why did the operation
fail."

**EH4.** These two types use the same enum mechanism, but they carry different
information: `Option`'s `None` carries no data, `Result`'s `Err` does. This also means the
two types cannot substitute for each other: `Option` is enough to say whether a lookup's
result was found; `Result` is needed when it is necessary to say why an operation failed.
A codebase can see both together — a `Result<Option<T>, E>`, for instance, carries both
the possibility that the operation failed and the possibility that, even on success, no
value was found; this combination is not measured in this lesson, but it clarifies why
the two types are kept separate. If a function's return type is `Result<i32, NumberError>`,
the caller does not just ask "did it fail" but can also read which error occurred. The
`NumberError` used in this lesson is an empty struct that carries no fields — the error
itself does not matter in this lesson, what matters is which arm runs when. The Custom
Error Types lesson will focus on the error itself, on the information it carries.

In the `match` above, the `Err` arm does not bind the error itself to a name; it writes
`Err(_)` — the underscore is a form of the catch-all pattern established in the Pattern
Matching lesson, and it is the source's way of saying "I will not read this value."
Because `NumberError` carries no fields, there is nothing to read here anyway; once the
error starts carrying information in the next two lessons, `Err(_)` will give way to a
pattern that actually binds the error.

`Option`'s `_or` / `_or_else` naming pattern continues on `Result` too: `unwrap_or`,
`unwrap_or_else`, `map_or`, `and_then` — all of them have a `Result` counterpart, and all
of them carry the same eager/lazy distinction. But this lesson does not measure those
methods; it measures the side that is unique to `Result`, the case where the two arms
**do different jobs**. In a method like `unwrap_or`, both "sides" would do the same job
of the same type (producing a value); here the `Ok` arm continues a computation, and the
`Err` arm stops the computation and produces a reason — two different jobs.

A function returning `Result` does not mean that function must resolve its own error
internally. `resolve` produces its own `Err` and returns it; the caller can decide to use
that value, ignore it, or carry it unchanged into its own return value. This third
option — carrying an error upward without processing it — can cross several function
boundaries in a row, and does not require writing a new arm in the source at each
crossing. This lesson does not measure that carrying; it only counts, at a single
boundary, over a single `match`, how many times the two arms run. The carrying itself is
the subject of the Shorthand for Error Propagation lesson.

## Which Arm Runs

When a function returns `Result`, the caller usually separates the two arms with a
`match`. Both arms are ordinary Rust code — neither runs through a separate mechanism
from the other, both are part of the same `match` expression that looks at the same
function's return value. This means both arms sit side by side in the compiled binary,
and which one runs depends only on the returned value (`Ok` or `Err`). The run below
calls the same function with three different inputs and counts which arm runs on each
call.

```rust
// error_check.rs — success and failure arms as separate bodies
pub type Ledger = std::rc::Rc<std::cell::RefCell<std::collections::BTreeMap<String, u32>>>;

pub fn record(ledger: &Ledger, name: &str) {
    *ledger.borrow_mut().entry(name.to_string()).or_insert(0) += 1;
}

pub fn times(ledger: &Ledger, name: &str) -> u32 {
    *ledger.borrow().get(name).unwrap_or(&0)
}

#[derive(Debug)]
pub struct NumberError;

pub fn resolve(text: &str) -> Result<i32, NumberError> {
    match text.parse::<i32>() {
        Ok(number) => Ok(number * 2),
        Err(_) => Err(NumberError),
    }
}
```

```rust
// two_arms.rs — the same function is called three times, counting which arm runs
mod error_check;
use error_check::{Ledger, record, times, resolve};
use std::cell::RefCell;
use std::rc::Rc;
use std::collections::BTreeMap;

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

    for text in ["8", "twenty-one", "15"] {
        match resolve(text) {
            Ok(result) => {
                record(&ledger, "success-arm");
                println!("{:<10} success arm, result {}", text, result);
            }
            Err(_) => {
                record(&ledger, "failure-arm");
                println!("{:<10} failure arm", text);
            }
        }
    }

    println!();
    let total = times(&ledger, "success-arm") + times(&ledger, "failure-arm");
    println!("total calls: {}", total);
    println!("success-arm: {} times", times(&ledger, "success-arm"));
    println!("failure-arm: {} times", times(&ledger, "failure-arm"));
}
```

```
8          success arm, result 16
twenty-one failure arm
15         success arm, result 30

total calls: 3
success-arm: 2 times
failure-arm: 1 times
```

`resolve` is called three times, and the total call count is fixed: three. But these
three calls' arms are not distributed the same way: two calls fall to `success-arm`, one
call to `failure-arm`. **EH5.** This shows that the two bodies written in the
source — the `Ok` arm's body and the `Err` arm's body — run independently of each other:
while the total stays fixed, which arm is chosen how many times depends only on the
input. The text `"twenty-one"` cannot be parsed as an integer, so the `Err` arm runs;
`"8"` and `"15"` can be parsed, so the `Ok` arm runs. Both arms are written at equal
length in the source, but how much each one runs in a given run is a number that depends
on the input.

This distinction sits in a different place from what was seen with `Option`. The
difference measured in the previous lesson was between an *eager* and a *lazy* body —
both could do the same job, and they differed only in run timing. Here, the two arms do
entirely different jobs: one doubles the result, the other produces an error value. The
count itself does not change this difference — the `times` function does not know what
job each arm does, it only counts how many times it was called. The measurement stays
the same here too: how many times a body written in the source runs in a given run.

These three inputs stand in for a validation pipeline: three separate fields entered by a
user, parsed in turn. In a real validation pipeline, inputs do not arrive one at a time
but as a list, and the same `resolve` call repeats for every field. The `for` loop above
shows this pattern at a small scale: three pieces of text pass through the same function,
and each one selects its own arm. The loop itself is not a single "run" — it is the sum
of three separate calls. What matters for this lesson is that each step of the loop is an
independent call, and each call selects its own arm without being affected by earlier
calls: the second call returning `Err` has no effect whatsoever on which arm the third
call selects. The counter ledger naturally preserves this independence — each `record`
call only increments its own key and touches no other key.

The `match` expression itself carries a principle established earlier, by the Control
Flow lesson: `match resolve(text) { ... }` is an expression, and it produces a
value — here, the value produced is `()`, because both arms only produce a side effect (a
counter record and a print) without visibly returning a value. This shows again the
difference between `match` itself being an expression and what value its arms produce:
the Control Flow lesson established that `match` is an expression and that all its arms
must yield the same type; here every arm produces `()`, but the body *inside* each arm is
entirely different from the other's. An expression itself may produce a single value,
even while the code that runs to reach that value diverges arm by arm.

## A Limit on the Measurement

Both arms are written in the source, but only one of them runs in a given call. The run
below makes this concrete with a single call.

```rust
// single_call.rs — both arms are written in the source, but only one runs in a single call
mod error_check;
use error_check::{Ledger, record, times, resolve};
use std::cell::RefCell;
use std::rc::Rc;
use std::collections::BTreeMap;

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

    match resolve("100") {
        Ok(result) => {
            record(&ledger, "success-arm");
            println!("result: {}", result);
        }
        Err(_) => {
            record(&ledger, "failure-arm");
            println!("an error occurred");
        }
    }

    println!("success-arm: {} times", times(&ledger, "success-arm"));
    println!("failure-arm: {} times", times(&ledger, "failure-arm"));
}
```

```
result: 200
success-arm: 1 times
failure-arm: 0 times
```

**EH6.** Because the `resolve("100")` call gives an `Ok` result, only `success-arm` runs;
`failure-arm` stays at zero. But this zero does not mean the `Err` arm *does not
exist* — that arm stands in the compiled binary, the compiler produced it, `match` was
required to cover it under exhaustiveness. It was never selected in this
particular run. An arm being written in the source and an arm having run in a given run
are two separate facts: one is a property of the source code, the other is the result of
a particular run made with a particular input. This distinction between "covered
code" — code the compiler accepts, satisfying the exhaustiveness principle — and
"executed code" — code actually run by the processor in a particular execution — also
summarizes the difference between reading a program's source and measuring one of its
runs.

The importance of this distinction becomes clearer when you consider what the
exhaustiveness principle actually provides. Without the exhaustiveness principle — that
is, if `match` allowed writing only the `Ok` arm and ignoring the `Err` case — a body
called `failure-arm` might never exist in the source at all, and when an input like
`"twenty-one"` arrived, the program would fall into an undefined state: no error value
would be produced, and the flow would not know where to go. The exhaustiveness principle
prevents this — the `Err` arm *must* exist in the source, whether it never runs in a
given run or takes over the entire run. Having run zero times does not mean it was not
needed there; on the contrary, the reason zero can even be measured is that the arm is
already there, standing ready to run.

This difference may not look significant in a small example, but it gains importance as
the number of call sites for a function grows. If a function is called thousands of times
and most of the inputs are valid, the `Err` arm still takes up space in the source, gets
compiled, gets checked — but may almost never run in a given run. The reverse is also
possible: if a function is called mostly with invalid inputs, the `Ok` arm may stand in
the source but run rarely in a given run. The source itself does not say which arm will
run more often; only an actual run can say that.

In the second run above, two of the three inputs succeeded and one failed —
`success-arm` ran twice, `failure-arm` once. If the same function were called with a
thousand inputs and only a few of them were invalid, the ratio would shift accordingly:
`success-arm` could sit near a thousand while `failure-arm` stayed in the single digits.
The length of the two arms written in the source has nothing to do with this ratio — the
`Err` arm could be a one-line body and still take over nearly the entire run, if the
inputs call for it. This lesson does not try to predict that ratio; it only shows that
the ratio must be read from a run, not from the source.

The two runs above together partly answer the question asked at this topic's opening: in
`Option`, whether the fallback body ran depended on whether it was written *eagerly or
lazily*; in `Result`, whether an arm runs depends on which input it corresponds to. Both
fit the same measurement frame — how many times a body written in the source runs in a
given run — but each falls into the "every time," "only when needed," or "never" class
for its own reason.

## Summary

- `Result<T, E>`, unlike `Option`, also carries the *reason* for failure: `Ok(T)` and
  `Err(E)` hold separate information; `Option`'s `_or` / `_or_else` naming pattern applies
  here too.
- The success and failure arms' bodies are written separately in the source and run
  independently of each other; when the same function is called repeatedly, the **total
  call count stays fixed**, but the arm counts diverge with the input.
- An arm being written in the source does not mean it runs on every run — in a single
  call, only one of the two arms runs, and the other runs **never** in that run.
- "Covered code" (code the compiler accepts, written to satisfy exhaustiveness) and
  "executed code" (code that actually runs in a given run) are two separate facts.
- A catch-all pattern like `Err(_)` is enough to count which arm ran without reading the
  error's content; the information an error carries is the subject of the next lessons.
- How often each arm will run cannot be read from the source; it can only be read from a
  run made with real inputs. In this run, both arms ran **only when needed**: each call
  ran the arm its own input required, and the other one ran **never** on that call.

## Next Step

In both runs above, the success arm repeated the same three lines: parse the value,
report if there is an error, otherwise continue. This pattern quickly turns into one that
repeats in a function that does several parses in a row — especially when a function has
to call another parse internally and check its result too, the same `match` skeleton
gets written several times in a row. The Shorthand for Error Propagation lesson measures
a form that shortens this pattern to a single mark — and shows that the shortcut shortens
the source but does not change the run, meaning `success-arm` and `failure-arm` give the
same counts seen in this lesson. The shorthand reduces not the count being measured, but
the number of lines that must be written to reach that count — this lesson built the
`match` skeleton by hand, the next lesson measures how much the same skeleton can be
shortened in the source without changing the run.
