---
title: 'Expressing Absence with a Type'
source: 'https://academia.sh/en/courses/rust-abstraction/option'
course: 'Traits, Generics and Error Handling'
language: en
updated: '2026-09-15T04:58:42+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Compares how many times an eager and a lazy default body run by executing them'
    - "Explains why the Option type carries a value's absence as a separate type"
    - 'Shows that for a cheap default body, the two forms produce the same result but a different count'
---

# Expressing Absence with a Type

Carrying the possibility that a value is absent in a type, and measuring whether the fallback body that covers that value — depending on whether it is written eagerly or lazily — runs even while the value already exists.

The Operator Overloading lesson closed out the Traits and Generics topic by measuring
that every use of an operator is actually a call, and that the operator body ran twice in
a chained sum. That lesson showed that a body written in the source runs exactly as many
times as it is used — every use was an unavoidable run. This lesson carries the same
question to a completely different place: if a body runs only *when a value is missing*,
does it always run, or can it sometimes never run at all?

The Interfaces, Errors and Generics course established that an error is itself a return
value, one that can be carried without leaving the normal control flow; what it measured
there was *where* an error is carried — not in a signature, but in the return type. This
lesson inherits that decision, but looks at it from a different axis: how many times does
the body that covers a value's *absence* — the fallback that kicks in — run? This topic's
(`error-handling`) five lessons apply this question in turn to absence, failure, the
shorthand, conversion, and the unrecoverable case; this first lesson looks only at
absence, and failure — which carries a reason — comes in the next lesson.

Throughout the Traits, Generics and Error Handling course, the same question stays the
same: how many times does a body written in the source run at execution? The Traits and
Generics topic asked this question of a behavior's implementations; in this topic the
question goes to the two types that carry an operation's outcome — `Option` and
`Result` — and to the shorthand, conversion, and unrecoverable case that follow them. The
question itself does not change, only where it is applied does.

## A Type for Absence

In Rust, `Option<T>` is used everywhere a value might not be present. It is the standard
library's own instance of the enum mechanism set up in the previous course: it has two
variants, `Some(T)`, which carries the value, and `None`, which carries none at all. If a
function's return type is `Option<i32>`, the caller must handle both cases at compile
time — accidentally trying to read a value out of `None` is not structurally possible in
this type.

**EH1.** This differs from carrying the "no value" case as a separate sentinel value (a
zero, say, or an empty string). A sentinel value can be confused with a real one; `None`,
by contrast, is a state completely separate from the `T` type that `Some(T)` carries, and
the compiler never confuses the two. This lesson does not re-establish that
distinction — it was already covered in the Ordered and Set Structures lesson — it only
looks at how many ways there are to *extract* a value from an `Option`, and how many
times each of those ways runs the body written in the source.

`Option` is mostly not built by hand; it arises as the natural result of a lookup. When an
ordered map is asked for a key's value, it returns `Some(value)` if the key is there and
`None` if it is not — the caller never has to ask "does the key exist" separately,
because the lookup itself produces a value that carries that information. The same
pattern holds for an out-of-bounds index into an array: direct indexing panics on an
invalid index, but a `get` call returns an `Option`, and the caller handles the
out-of-bounds case as an ordinary `None`. The examples used in this lesson do not
simulate these lookups; they build `Some` and `None` values directly, because what is
measured is not the lookup itself but the way a value is extracted from its result.

The Pattern Matching lesson established that a `match` expression must cover every
arm — the exhaustiveness principle. A `match` written by hand over an `Option` follows
the same principle: both the `Some` and `None` arms must be written, and if either is
left out, the source does not compile at all. `unwrap_or` and `unwrap_or_else` are
shortcuts that fold this two-armed `match` into a single method call — the
`Some(value) => value, None => fallback` form itself does not appear in the source, but
the code the compiler generates still branches into the same two arms. This lesson
measures how many times those two arms run, directly through these shortcuts, without
writing the `match` form again.

The same exhaustiveness principle applies to the `if let` form too, but in a different
way: `if let Some(value) = option { ... }` writes only the `Some` arm and silently skips
the `None` case — this does not violate exhaustiveness, because `if let` is already
defined by the language itself as a *partial* form of `match`. `unwrap_or` and
`unwrap_or_else` stand at the opposite end: both cover both arms, and they differ only in
how the `None` arm's body is written in the source. This lesson does not measure
`if let`, because it has no "arm that does not run" in the source to begin with — the
difference being measured is how differently two methods that both write both arms
actually run.

## Eager and Lazy Defaults

The two most common ways to pull a value out of an `Option` both supply a fallback that
kicks in when the value is missing. That fallback can be written in two forms: directly,
as a value (`unwrap_or`), or as a call that returns a closure (`unwrap_or_else`). Both
read as if they do the same job, but when their arguments are evaluated differs.

The Rust's Design Goals lesson established that an expression is evaluated to produce a
value; a function call's argument is itself an expression, and it is evaluated before the
call happens. The form `unwrap_or(eager_default(&ledger))` is not exempt from this rule:
`eager_default(&ledger)` is an argument expression, and it is computed before `unwrap_or`
itself runs — even though it looks as if there is a branch inside `unwrap_or` checking
whether it is `Some` or `None`, the argument has already been evaluated before that
branch. `unwrap_or_else`, on the other hand, takes not a *value* as its argument but a
*closure*; the closure itself does not run when it is built, only when
`unwrap_or_else`'s own internal implementation enters the `None` arm is it called.

In the run below, `full` and `empty` are built by hand, but both represent the two
outcomes a lookup can return: `full` stands for a lookup whose key was found, `empty` for
one that was not. The bodies being measured — the `eager_default` function and two
closures — each report their own run by dropping a record into the counter ledger. The
run below shows this difference directly.

```rust
// absence_ledger.rs — body-counting core: the Ledger/record/times slice of times.rs
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)
}
```

```rust
// absence_measurement.rs — how many times the eager and lazy defaults run
mod absence_ledger;
use absence_ledger::{Ledger, record, times};
use std::cell::RefCell;
use std::rc::Rc;
use std::collections::BTreeMap;

fn eager_default(ledger: &Ledger) -> i32 {
    record(ledger, "eager-default");
    -1
}

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

    let full: Option<i32> = Some(7);
    let empty: Option<i32> = None;

    let a = full.unwrap_or(eager_default(&ledger));
    let b = full.unwrap_or_else(|| {
        record(&ledger, "lazy-default-unneeded");
        -1
    });
    let c = empty.unwrap_or_else(|| {
        record(&ledger, "lazy-default-needed");
        -1
    });

    println!("full.unwrap_or(...) result: {}", a);
    println!("full.unwrap_or_else(...) result: {}", b);
    println!("empty.unwrap_or_else(...) result: {}", c);
    println!();
    for name in ["eager-default", "lazy-default-unneeded", "lazy-default-needed"] {
        println!("{:<28}{} times", name, times(&ledger, name));
    }
}
```

```
full.unwrap_or(...) result: 7
full.unwrap_or_else(...) result: 7
empty.unwrap_or_else(...) result: -1

eager-default               1 times
lazy-default-unneeded       0 times
lazy-default-needed         1 times
```

Even while `full` is full, `eager_default` still runs once: `unwrap_or`'s argument is
evaluated as an ordinary function call before the call itself happens. Whether the
`Option` is `Some` or `None` makes no difference here — because the argument is an
expression, it is computed regardless of whether it will be used. **EH2.** That is why
the `eager-default` line falls into the "every time" class: whenever `unwrap_or` is
called, its argument is unavoidably run as well.

The closure given to `unwrap_or_else`, by contrast, behaves the opposite way. Called on
`full`, its body never runs at all — `lazy-default-unneeded` stays at zero — because the
closure is only called in the `None` case. Called on `empty`, that same closure pattern
actually runs, and `lazy-default-needed` climbs to one. The difference lies in the
argument's kind: one is a ready-made value, the other a call that *produces* the value
only when needed. `lazy-default-needed` falls into the "only when needed" class — it runs
exactly as often as the absence actually occurs.

What the Interfaces, Errors and Generics course measured was *where* an error is bound:
in a signature, at the call site, or inside the value. This lesson does not ask that
question — `Option` itself already belongs to the same family as that course's third
option, a state carried inside a value. What is measured here is how many times the body
that covers that state runs; even though `eager-default` and `lazy-default-needed`
produce the same count (one), one runs unconditionally and the other only when it is
truly needed.

The `_or` and `_or_else` suffixes in the method names label this distinction: the `_or`
suffix marks methods where a ready-made value is given directly as the argument; the
`_or_else` suffix marks methods where a closure is given in its place. This naming
pattern is not unique to `Option` — the standard library uses the same `_or` / `_or_else`
pair on other types too, and the meaning stays the same in both: `_or` is eager,
`_or_else` is lazy. The next lesson will meet this naming pattern again on `Result`.

The `Option` type's standard-library methods split along this same axis. Ones like
`unwrap_or` that want a ready-made value always run; ones like `unwrap_or_else` that want
a closure run only when needed. A chaining method like `and_then` stands only on the
closure side: the closure it is given runs only in the `Some` case and is never called on
`None`. `map_or` combines both in a single call — its first argument is a ready-made
value and is evaluated every time, even on `None`; its second argument is a closure and
runs only in the `Some` case; inside one single method call, the two classes this lesson
measures can be seen side by side. A method name not ending in `_else` does not mean this
distinction stops applying; exactly which argument each method runs and when is
documented for every method, and it can be read through the same axis measured in this
lesson.

## A Limit on the Measurement

The count difference above does not mean the lazy form is always the better choice. The
difference is only in *how many times it runs*; if the body is cheap, the outcome of both
forms is indistinguishable.

```rust
// cheapness.rs — when the body is cheap, both forms produce the same result, only the count differs
mod absence_ledger;
use absence_ledger::{Ledger, record, times};
use std::cell::RefCell;
use std::rc::Rc;
use std::collections::BTreeMap;

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(BTreeMap::new()));
    let full: Option<i32> = Some(9);

    let a = full.unwrap_or({
        record(&ledger, "cheap-eager");
        0
    });
    let b = full.unwrap_or_else(|| {
        record(&ledger, "cheap-lazy");
        0
    });

    println!("eager form's result: {}", a);
    println!("lazy form's result: {}", b);
    println!("cheap-eager body: {} times", times(&ledger, "cheap-eager"));
    println!("cheap-lazy body: {} times", times(&ledger, "cheap-lazy"));
}
```

```
eager form's result: 9
lazy form's result: 9
cheap-eager body: 1 times
cheap-lazy body: 0 times
```

**EH3.** Both lines give the same result (`9`); the program is not affected by which form
is chosen. The only thing that diverges is whether the body runs: `cheap-eager` runs
once, `cheap-lazy` never runs. Because the body here only returns an integer, this
difference changes nothing; but if the body were an expensive operation — a file read, a
network call, a large computation — the same difference would turn into a cost that can
no longer be overlooked. This lesson does not measure that cost — it only counts *how
many times it runs*; the size of the cost depends on the body written in the source
itself, and this lesson does not evaluate that.

This is where the measurement's real limit shows up. If a program fills in a missing
field from a settings record with a fixed integer, there is no visible difference between
the eager and the lazy form — both produce the same number, and the only difference is as
small as the counter record seen in the run above. If that same program instead fills the
missing field by reading a file or computing a sum, the eager form runs that read or
computation even while the value already exists — the result still does not change,
because `unwrap_or` will not use the computed value anyway, but the body still runs. This
lesson does not decide which body counts as "cheap" and which as "expensive"; it only
counts how many times each form runs and leaves that decision to whoever reads the
source. The count alone does not say that one form is "better" than the other — it only
makes visible what runs and what does not.

In both runs above, the "eager" side was always written as a separate function, but that
is not required. The block in the form `unwrap_or({ record(&ledger, "cheap-eager"); 0 })`
is itself an expression too; it is evaluated as the argument even without carrying a
separate function name. The limit being measured is therefore not specific to a function
call — no matter how `unwrap_or`'s argument is written, as long as it is an expression, it
is evaluated before the call happens. The only difference is readability: a separate
function name names what the body does; an inline block leaves the same work unnamed.

These three runs — the eager/lazy comparison, the cheap-body comparison, and the naming
difference between them — together arrive at a single conclusion: which method is chosen
for extracting a value from an `Option` is a question independent of the result. The
result is the same in both forms; what diverges is only whether the body written in the
source runs, and how many times. This distinction may look like a small detail, but it
will be met again in the same form throughout the next four lessons: depending on which
method of a type is chosen, the same body in the same source sometimes runs unavoidably,
sometimes only when needed, and sometimes not at all.

## Summary

- `Option<T>` carries the possibility that a value is absent as a separate type;
  `Some(T)` and `None` are handled separately at compile time, and a `match` must cover
  both under the exhaustiveness principle.
- The argument given to `unwrap_or` is an expression and runs **every time**, even while
  the value already exists — it does not look at which variant the `Option` is.
- The closure given to `unwrap_or_else` runs only in the `None` case: it runs **never**
  while the value exists, and **only when needed** once absence actually occurs.
- Some methods, like `map_or`, combine both classes in a single call — one argument runs
  every time, the other only when needed.
- If the body is cheap, the result of both forms is the same; the difference is only in
  the run count, and it matters more as the body gets more expensive.
- The `_or` / `_or_else` naming pattern marks the eager/lazy distinction across the
  standard library; it will appear again on `Result` in the next lesson.

## Next Step

This lesson measured the case where a value might be *absent*. But when an operation
fails, saying just "absent" is usually not enough — the *reason* for the failure has to
be carried too. `None` has no reason; it does not say why an operation failed, only that
a value was not found. The Failure as a Return Value lesson applies the same "how many
times did it run" question to a type that now also carries a reason: when the success
and failure arms are written as separate bodies, and the same function is called several
times in a row, which of those arms runs in a given run, and how do the arm counts
diverge while the total call count stays fixed? This topic's name (`error-handling`)
starts earning its real meaning from that lesson on: what is measured is no longer just a
value's absence, but a named failure.
