Skip to content

← Errors Are Values step 9 of 24

Medium Primitives

Converting between Option and Result

Read an optional setting out of a list of key/value pairs.

pub fn read_optional_port(
    pairs: Vec<(String, String)>,
    key: String,
) -> Result<Option<u16>, String>

Three outcomes, and the signature encodes all three:

situation result
the key is absent Ok(None)
present and parses as u16 Ok(Some(n))
present and does not parse Err(…)

The error message is exactly:

key `port` is not a valid port: eighty

— the key in backticks, then the original value (untrimmed) after the colon. The value is trimmed before parsing, so " 443 " is Ok(Some(443)), and the first matching key wins.

Read that table once more, because the signature is the lesson. “Missing” and “invalid” are different things. A config reader that returns Err for a setting the user simply did not set is a config reader that cannot have defaults. Result<Option<T>, E> says “getting the answer can fail, and the answer may legitimately be nothing” — two independent facts, two layers.

The starter’s E0308 is the whole problem in miniature

error[E0308]: mismatched types
   |
   |             Ok(n) => Some(n),
   |                      ^^^^^^^ expected `Result<Option<u16>, String>`,
   |                              found `Option<u16>`

You have an Option from the lookup and a Result from the parse, and the inner match produces one of each. Every beginner’s first instinct is to nest matches until the types happen to line up, and it works, and it is four levels deep for something that should be a sentence.

The natural intermediate value here is:

Option<Result<u16, String>>

“maybe there is a value, and if there is, parsing it may have failed”. Which is almost the return type, with the layers in the wrong order. Swapping them is a single method.

transpose, in both directions

let a: Option<Result<u16, String>> = Some(Ok(8080));
a.transpose()                        // Ok(Some(8080))

let b: Option<Result<u16, String>> = Some(Err("nope".into()));
b.transpose()                        // Err("nope")

let c: Option<Result<u16, String>> = None;
c.transpose()                        // Ok(None)     <- the interesting one

let d: Result<Option<u16>, String> = Ok(Some(1));
d.transpose()                        // Some(Ok(1))

It exists on both types (Option::transpose since 1.33, Result::transpose too) and the direction is genuinely confusing until you have seen the four lines above. The one worth burning in is None.transpose() == Ok(None): an absent value is not an error, so transposing it produces a successful “nothing”.

The rest of the bridge

Option → Resultok_or(err) and ok_or_else(f). None becomes Err(err), Some(v) becomes Ok(v). Same eager/lazy distinction as unwrap_or: if building the error allocates (and a format! does), use ok_or_else.

Result → Option.ok() keeps the value, .err() keeps the error.

Clippy has two default-on lints aimed squarely at beginners here, and they are worth knowing before you write them:

error: called `ok().expect()` on a `Result` value
   = help: you can call `expect()` directly on the `Result`

ok_expect and err_expect. r.ok().expect(msg) is a thing people write because they are thinking “convert, then unwrap” — but going through Option throws the error away first, so the panic message is worse than it would have been. The direct r.expect(msg) prints your message and the error.

manual_ok_or is the third: it fires on opt.map_or(Err(e), Ok) and suggests ok_or(e). Be aware that it only catches that one shape — the equivalent hand-written match slips through. The gate here will not catch a match-based solution for you, so this is a case where you have to hold yourself to the standard: write the combinator chain.

Notes

  • 70000 does not fit in a u16 (max 65535), and u16::from_str reports that as a ParseIntError just like "eighty" does. Same handling, no special case.
  • Present-but-empty ("") is an error, not an absence. The distinction is real: an empty value in a config file is usually a typo, and silently treating it as unset hides the typo.
  • iter_filter_is_ok and iter_filter_is_some are the lints for .filter(Result::is_ok) followed by unwrapping — you want flatten() or filter_map, which you will meet properly when this track reaches iterators.

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

Loading visualization…