Option<T> represents presence or absence; Result<T, E> represents success or failure with an error E. Choose based on whether absence is normal. A missing optional setting may be None, whereas a failed file read generally needs an error explaining why.
Returning Result does not promise that a function never panics. It describes failures reported through the return value. Ignoring a Result normally triggers a must_use warning, not an unavoidable recovery mechanism or proof of correct handling. Result
An Example: An Optional Port Setting
Turning both missing and invalid settings into defaults can silently hide configuration errors. Here absence uses a default, while invalid syntax or values outside u16 must fail. Whether port 0 is allowed is a separate business validation step.
map first converts Option<&str> into Option<Result<u16, E>>. transpose exchanges the container layers without losing any of the three cases. The outer Result lets callers propagate failure before deciding how to handle absence. Option::transpose
Successful Parsing Does Not Establish the Business Range
The representable u16 range and a service's accepted range differ. If this service rejects port 0, model syntax and business failures separately rather than requiring callers to inspect error strings.
usestd::num::ParseIntError;#[derive(Debug)]enumPortError{ Syntax(ParseIntError), Zero }fnport(input:Option<&str>)->Result<u16, PortError>{let parsed = input.map(str::parse::<u16>).transpose()
.map_err(PortError::Syntax)?.unwrap_or(8080);if parsed ==0{returnErr(PortError::Zero);}Ok(parsed)}fnmain(){assert_eq!(port(None).unwrap(),8080);assert_eq!(port(Some("9000")).unwrap(),9000);assert!(matches!(port(Some("0")),Err(PortError::Zero)));matchport(Some("oops")){Err(PortError::Syntax(source))=>assert!(!source.to_string().is_empty()),
other =>panic!("expected syntax error, got {other:?}"),}}
Some binding APIs use port 0 to request an OS-assigned port. Rejecting it here is service policy, not a numeric parsing rule. Separating the layers preserves parser reuse while stating application constraints explicitly.
Which three cases does transpose preserve?
Transposition does not merge states; it changes which layer the caller handles first. Try all three inputs: absence and invalid text take different paths, so ? can propagate errors while unwrap_or supplies a default only for normal absence.
Preparing the visual
Which three cases does transpose preserve?
None becomes Ok(None), Some(Ok(v)) becomes Ok(Some(v)), and Some(Err(e)) becomes Err(e).
map's closure returns an ordinary value; and_then's closure returns the same kind of container, avoiding unwanted nesting. Not every combinator operates on success: or_else runs on absence or failure.
unwrap_or(expensive()) evaluates its argument before the call, even when a value exists. unwrap_or_else(|| expensive()) invokes the closure only when needed. A fallback deliberately consumes absence or failure; choose that policy explicitly. Option
The same distinction applies to ok_or versus ok_or_else and or versus or_else. A fallback that allocates, logs or reads configuration can cause unwanted work and side effects when evaluated eagerly. A simple constant need not become a closure merely for stylistic consistency.
? Returns Early at the Current Return Boundary
In an ordinary Result-returning function, ? extracts Ok or converts Err through the applicable From conversion and returns early. It does not try recovery strategies or log the failure.
In an Option-returning function, None returns None early. Applying ? directly to Option in a Result-returning function does not work: use ok_or or ok_or_else to define the corresponding error. In the opposite direction, .ok() discards the error cause.
The general language mechanism propagates residual values; not every type supporting ? should be described through Result's From conversion. The concrete Result and Option rules are usually sufficient for everyday reasoning.
Early return normally drops the relevant local values, but it does not undo requests already sent, file writes or committed transactions. Atomic updates need their own commit protocol.
? Inside a Closure Returns to That Closure's Caller First
usestd::num::ParseIntError;fnprepare(input:&str)->Result<u32, ParseIntError>{letrun=||->Result<u32, ParseIntError>{let value = input.parse::<u32>()?;Ok(value)};let result =run();// Execution continues here; the outer function handles the closure result.
result.map(|value|value.saturating_add(1))}fnmain(){assert_eq!(prepare("7").unwrap(),8);assert_eq!(prepare("4294967295").unwrap(),u32::MAX);assert!(prepare("bad").is_err());}
A Result-returning closure passed to map adds another container layer. Decide whether you intend to store a potentially failing result or use and_then to chain and propagate the same container. Indentation alone does not identify the return boundary affected by ?.
Early Return Does Not Undo Changes to Existing Objects
usestd::num::ParseIntError;fnappend_incrementally(target:&mutVec<u32>, input:&[&str])->Result<(), ParseIntError>{for text in input { target.push(text.parse()?);}Ok(())}fnappend_after_validation(target:&mutVec<u32>, input:&[&str])->Result<(), ParseIntError>{let parsed:Result<Vec<u32>, _>= input.iter().map(|text|text.parse()).collect();
target.extend(parsed?);Ok(())}fnmain(){letmut first =vec![9];assert!(append_incrementally(&mut first,&["1","bad"]).is_err());assert_eq!(first,[9,1]);letmut second =vec![9];assert!(append_after_validation(&mut second,&["1","bad"]).is_err());assert_eq!(second,[9]);append_after_validation(&mut second,&["1","2"]).unwrap();assert_eq!(second,[9,1,2]);}
The second implementation promises not to change target on a parse error and uses temporary storage to provide that behavior. It is not a general database transaction, nor does it turn allocation failure or process crashes into recoverable errors. Document the state after failure rather than only saying “returns Result.”
Borrow the Container to Avoid Unnecessary Moves
Many combinators take self by value. Use as_ref first when the original container is still needed: this produces Option<&T> or Result<&T, &E>. Option::as_deref produces Option<&str>, suitable for read-only parsing.
fnmain(){let setting =Some(String::from("9000"));let borrowed:Option<&str>= setting.as_deref();assert_eq!(borrowed,Some("9000"));assert_eq!(setting.as_ref().unwrap().len(),4);assert_eq!(setting.unwrap(),"9000");// Only now consume the container.
}
Batch Processing: Stop on Failure or Collect Every Result?
collect::<Result<Vec<_>, _>>() short-circuits at the first Err and does not return the collected success prefix. To report every bad line, retain individual Results and input positions. filter_map(|s| s.parse().ok()) discards every failure, which is appropriate only when ignoring invalid input is an explicit policy.
Here filter_map derives error line numbers only after the complete results have been retained; the original errors remain available. Independent field validation can likewise collect multiple failures. If field B depends on valid field A, however, substituting a default and continuing may produce misleading secondary errors.
Large inputs may make storing every result expensive. Emit diagnostics incrementally, cap error counts or retain positions and summaries. Stopping at the first error, reporting all errors and importing partial successes are different product behaviors that deserve explicit interfaces.
Error Reporting from main
main can return Result<(), E> when E implements Debug. The standard Termination implementation prints Err's debug representation and returns a failure exit code; its portable contract does not fix that code to the integer 1. For stable user messages and distinct exit codes, match explicitly and return ExitCode. Termination
unwrap and expect suit test assertions or proven invariants, not routine external input failures. For error types, retained causes and business context, continue with Error and its ecosystem.