Result<T, E> & Option<T>: The ? Operator Lowering and From Conversions
- Last updated
- Prerequisites:
- Rust enums, generics, and traits
- Understanding of error propagation patterns
- rust
- error-handling
- result
- option
- question-mark
- from
Read at your depth
The practical view
Option<T> (Some/None) and Result<T, E> (Ok/Err) are the standard way to handle optional and fallible operations in Rust. The ? operator propagates errors: fn read() -> Result<String, io::Error> { let s = fs::read_to_string(path)?; Ok(s) } — if the inner call returns Err, ? returns it from the function immediately. Option also supports ?: in a function returning Option<T>, None propagates. The From trait enables automatic error conversion: if your function returns Result<_, MyError> and a ? encounters an io::Error, the compiler inserts From::from(io::Error) -> MyError (if implemented). Use map, and_then, unwrap_or, match for handling without early returns.
The same idea in other frameworks
N/A — Rust language feature.N/A — Rust feature.N/A — Rust feature.N/A — Rust feature.Legacy vs modern
Nested match error handling vs ? operator
Manually matching every error nests the code and obscures the happy path; ? flattens it into a linear flow with automatic conversion.
match fs::read_to_string(path) {
Ok(s) => match s.parse::<i32>() {
Ok(n) => Ok(n),
Err(e) => Err(e.into()), // ❌ OLD: manual conversion per level
},
Err(e) => Err(e.into()),
}fn read_number(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
let s = fs::read_to_string(path)?; // ✅ NEW: io::Error -> Box<dyn Error> via From
Ok(s.trim().parse::<i32>()?) // ✅ NEW: ParseIntError -> Box<dyn Error> via From
}Interview gotchas
Context
This tests the desugaring — interviewers want the exact match + early-return + From::from translation, which explains the type requirements of ?.
The mechanical answer
The ? operator desugars to a match with an early return: for Result, match expr { Ok(v) => v, Err(e) => return Err(From::from(e)) }; for Option, match expr { Some(v) => v, None => return None }. The From::from(e) is the crucial part: the enclosing function's error type must implement From<the inner error type>, and the compiler inserts the conversion automatically. This is why ? composes across different error sources: each inner error type converts into your unified error type via its From impl. If no From impl exists, the code doesn't compile (a compile-time guarantee that error propagation is type-correct). The Try trait generalizes ? beyond Result/Option (for custom types implementing Try).
Trap
The naive answer is '? just unwraps and returns the error'. The From conversion is mandatory and automatic — if the error types don't convert, it's a compile error, not a runtime surprise. Another trap: thinking ? can be used in any function — it requires the function to return Result/Option (or Try); a function returning a plain value cannot use ?. Candidates who explain the From requirement show real understanding.
Context
This tests the cost model — interviewers want the enum representation, niche optimization, and the control-flow difference from exception-based error handling.
The mechanical answer
Option<T> and Result<T, E> are plain enums (tagged unions): the tag + the payload, laid out with niche optimizations where possible — Option<&T> is one pointer (null is the None niche), Result<&T, E> can be one pointer too. There's no heap allocation and no exception/unwinding machinery: errors flow as values through normal control flow. The ? operator is a match + early return, compiled to a branch. This contrasts with exceptions: exceptions require runtime unwinding machinery, hidden control-flow edges, and (in some languages) allocation of exception objects; the compiler cannot optimize code paths as freely because any call might throw. Rust's value-based errors make error paths explicit, visible to the optimizer, and testable — at the cost of requiring programmers to handle or propagate them explicitly (no silent 'let it bubble up' without a type signature).
Trap
The naive answer is 'errors are free'. The enum representation is cheap but not free (a branch per check), and multi-level propagation has some cost — but it's predictable and comparable to a success-path check, unlike exception overhead. Another trap: assuming Option/Result are heap-allocated — they are stack values, with niche optimization collapsing the size. Candidates who describe the enum layout and control-flow difference show real understanding.