The Result Type
What Is Result<T, E>?
Section titled “What Is Result<T, E>?”Result<T, E> is the standard Rust type for operations that can either succeed or fail in an expected way. It is defined in the standard library as:
enum Result<T, E> { Ok(T), Err(E),}Ok(T)— the operation succeeded and produced a value of typeT.Err(E)— the operation failed and produced an error value of typeE.
Both T and E are generic parameters you fill in. For example, Result<i32, String> succeeds with an i32 or fails with a String error message.
Returning a Result
Section titled “Returning a Result”To signal a recoverable failure from a function, change its return type to Result<T, E> and return Ok(value) on success or Err(error) on failure:
fn parse_age(s: &str) -> Result<u32, String> { s.parse::<u32>().map_err(|e| format!("parse error: {}", e))}The .map_err call converts the standard library’s parse error into your own String error type.
Handling a Result
Section titled “Handling a Result”The most explicit way to handle a Result is a match expression — it forces you to address both branches:
fn parse_age(s: &str) -> Result<u32, String> { s.parse::<u32>().map_err(|e| format!("parse error: {}", e))}
fn main() { match parse_age("25") { Ok(age) => println!("Parsed age: {}", age), Err(e) => println!("Error: {}", e), }}Convenience Methods
Section titled “Convenience Methods”For less verbose code, Result provides several methods:
| Method | Behaviour |
|---|---|
.unwrap() | Returns the Ok value; panics on Err |
.expect(msg) | Same, but with a custom panic message |
.unwrap_or(default) | Returns the Ok value or a default |
.unwrap_or_else(|e| ...) | Returns the Ok value or computes a default from the error |
.is_ok() / .is_err() | Boolean tests without extracting the value |
.map(|v| ...) | Transforms the Ok value, leaves Err unchanged |
.map_err(|e| ...) | Transforms the Err value, leaves Ok unchanged |
fn parse_age(s: &str) -> Result<u32, String> { s.parse::<u32>().map_err(|e| format!("parse error: {}", e))}
fn main() { // Match on Result to handle both branches match parse_age("25") { Ok(age) => println!("Parsed age: {}", age), Err(e) => println!("Error: {}", e), }
// .unwrap_or returns a default on Err let age1 = parse_age("abc").unwrap_or(0); println!("unwrap_or default: {}", age1);
// .unwrap_or_else lets you compute the default let age2 = parse_age("not_a_number").unwrap_or_else(|e| { println!("Recovering from: {}", e); 18 }); println!("unwrap_or_else result: {}", age2);
// is_ok / is_err for simple checks println!("parse_age(\"30\") is_ok: {}", parse_age("30").is_ok()); println!("parse_age(\"xx\") is_err: {}", parse_age("xx").is_err());}Compiling…
Converting Between Option and Result
Section titled “Converting Between Option and Result”Option<T> and Result<T, E> both represent “a value might not be here”, but they carry different information: Option just says “or nothing”; Result says “or here is why not”. Real code frequently needs to cross between the two:
.ok_or(err)/.ok_or_else(\|\| err)— turnOption<T>intoResult<T, E>:Some(v)becomesOk(v),NonebecomesErr(err)..ok()— turnResult<T, E>intoOption<T>, discarding the error entirely:Ok(v)becomesSome(v),Err(_)becomesNone..transpose()— swap the nesting:Option<Result<T, E>>becomesResult<Option<T>, E>(and back). Useful when you have “maybe a fallible thing” and need “fallibly, maybe a thing”.
fn parse_age(s: &str) -> Result<u32, String> { s.parse::<u32>().map_err(|e| format!("parse error: {}", e))}
fn main() { // Option -> Result: ok_or_else turns None into a computed Err let cached: Option<u32> = None; let age: Result<u32, String> = cached.ok_or_else(|| String::from("no cached age")); println!("ok_or_else on None: {:?}", age);
let present: Option<u32> = Some(40); println!("ok_or_else on Some: {:?}", present.ok_or_else(|| String::from("unreachable")));
// Result -> Option: .ok() discards the error entirely println!(".ok() on Err: {:?}", parse_age("nope").ok()); println!(".ok() on Ok: {:?}", parse_age("40").ok());
// transpose swaps Option<Result<T, E>> and Result<Option<T>, E> let maybe_result: Option<Result<u32, String>> = Some(parse_age("21")); let transposed: Result<Option<u32>, String> = maybe_result.transpose(); println!("transposed = {:?}", transposed);}Compiling…
What the Compiler Says: No Implicit Conversion Inside Result
Section titled “What the Compiler Says: No Implicit Conversion Inside Result”Result<T, E> is not covariant in any special way — Result<i32, String> and Result<String, String> are simply two different, unrelated types, exactly like i32 and String are. There is no implicit conversion, even though both are “a Result with a String error”:
// @expect-error E0308fn main() { let r: Result<i32, String> = Ok(5); let s: Result<String, String> = r; println!("{:?}", s);}error[E0308]: mismatched types --> result_mismatch.rs:3:37 |3 | let s: Result<String, String> = r; | ---------------------- ^ expected `Result<String, String>`, found `Result<i32, String>` | | | expected due to this | = note: expected enum `Result<String, _>` found enum `Result<i32, _>`
error: aborting due to 1 previous errorThis is the same “no implicit conversion” rule you already know from plain values — i32 never silently becomes i64, and Result<i32, E> never silently becomes Result<String, E>. If you need the Ok type to change, you must say so explicitly with .map(|v| v.to_string()).
The if r.is_ok() { r.unwrap() } Smell
Section titled “The if r.is_ok() { r.unwrap() } Smell”A Result already encodes “did this work?” in its variant. Checking .is_ok() and then separately calling .unwrap() re-derives information the compiler could have proven for you at the same point, and it throws away the Err value entirely if the check fails silently (or, worse, panics later if someone reorders the code):
fn parse_age(s: &str) -> Result<u32, String> { s.parse::<u32>().map_err(|e| format!("parse error: {}", e))}
fn main() { let r = parse_age("21"); if r.is_ok() { println!("age: {}", r.unwrap()); // re-checks what is_ok() already proved }}Every idiomatic alternative is both shorter and safer, because it lets the pattern match (or a combinator) prove the variant and extract the value in one step: if let Ok(v) = r { ... }, let Ok(v) = r else { return; }, or a combinator chain like .map(...). The if r.is_ok() version also silently does nothing on Err — there is no else branch reminding you that failure needs handling too.
let else and a Combinator Tour
Section titled “let else and a Combinator Tour”let else binds the success case and requires the else branch to diverge — return, break, continue, or panic! — so the rest of the function can assume the binding always holds:
fn parse_age(s: &str) -> Result<u32, String> { s.parse::<u32>().map_err(|e| format!("parse error: {}", e))}
fn report(s: &str) { let Ok(age) = parse_age(s) else { println!("could not report: invalid age string"); return; }; println!("age is {}", age);}
fn main() { report("30"); report("nope");}If the else block does not diverge, the compiler rejects it — the whole point of let else is that everything after it can treat the binding as unconditional:
// @expect-error E0308fn parse_two(a: &str) -> i32 { let Ok(x) = a.parse::<i32>() else { 0 }; x}
fn main() { println!("{}", parse_two("abc"));}error[E0308]: `else` clause of `let...else` does not diverge --> let_else_bad.rs:2:39 |2 | let Ok(x) = a.parse::<i32>() else { | _______________________________________^3 | | 04 | | }; | |_____^ expected `!`, found integer | = note: expected type `!` found type `{integer}` = help: try adding a diverging expression, such as `return` or `panic!(..)` = help: ...or use `match` instead of `let...else`
error: aborting due to 1 previous errorBeyond let else, map, and_then, and unwrap_or_else chain together to replace nested match/if entirely — map transforms a success value, and_then chains another fallible step (like Option’s flatten + map in one), and unwrap_or_else supplies a fallback computed from the error at the very end:
fn parse_age(s: &str) -> Result<u32, String> { s.parse::<u32>().map_err(|e| format!("parse error: {}", e))}
fn main() { // let-else: bind on Ok, or diverge (return/panic/continue) on Err let Ok(good_age) = parse_age("30") else { println!("unreachable in this demo"); return; }; println!("let-else bound: {}", good_age);
// Smell: if r.is_ok() { r.unwrap() } duplicates the check the type already encodes let r = parse_age("21"); if r.is_ok() { println!("smelly: {}", r.unwrap()); }
// Fix: combinator chain, no redundant check, no unwrap let doubled = parse_age("21") .map(|n| n * 2) .and_then(|n| if n > 100 { Err(String::from("too big")) } else { Ok(n) }) .unwrap_or_else(|e| { println!("falling back because: {}", e); 0 }); println!("doubled = {}", doubled);}Compiling…
flowchart TD
A[You have a Result] --> B{Want a fallback on Err?}
B -->|fixed value| C[.unwrap_or]
B -->|computed from the error| D[.unwrap_or_else]
B -->|discard the error, need Option| E[.ok]
F[You have an Option] --> G{Need a Result instead?}
G -->|fixed error| H[.ok_or]
G -->|computed error| I[.ok_or_else]
J[Have a Result, want to keep chaining] --> K[.map for the Ok value]
J --> L[.and_then for another fallible step] Mental Model
Section titled “Mental Model”Treat every Result you receive as a two-branch value the type system already forces you to acknowledge — the question is never “do I need to check this?” (you always do, the compiler will not let you forget) but “what is the cleanest way to acknowledge it here?” match is the ground truth every other tool desugars to; let else and combinators are shorthands for the common shapes of that match — “bind or bail” and “transform without unwrapping”, respectively. Reaching for .is_ok() plus .unwrap() is writing your own, worse version of a match arm the compiler would have checked for you.
Trade-offs
Section titled “Trade-offs”match is maximally explicit and reads correctly even to someone unfamiliar with the specific combinator names, at the cost of vertical space and nesting when you chain several fallible steps. Combinators (map, and_then, unwrap_or_else, ok_or_else) compress a chain of fallible transformations into a single expression, but each name is one more piece of vocabulary a reader must already know, and a long combinator chain can be harder to step through in a debugger than an equivalent match. let else sits in between: as readable as a match for the common “bind or bail” shape, but it only works when the failure branch can genuinely diverge — it is the wrong tool when failure should produce a fallback value instead of leaving the function.
Common Misconceptions
Section titled “Common Misconceptions”- “
.ok()is a safe way to check success without unwrapping.” It is safe from panics, but it is not free — it silently discards theErrvalue. If anything downstream needs to know why it failed (logging, retries, a user-facing message),.ok()has already thrown that information away. - “
if r.is_ok() { r.unwrap() }is fine becauseis_ok()guaranteesunwrap()cannot panic.” True in this exact shape, but it is strictly worse thanif let Ok(v) = r: it re-derives the variant twice, provides noelsebranch reminding you to handleErr, and breaks the moment someone inserts code between the check and theunwrap()that could changer. - “
let else’selseblock can do anything, as long as the program doesn’t crash.” It specifically must diverge —return,break,continue, or a panicking macro. A block that evaluates to a normal value (even(), if the rest of the function needs the binding) is a type error, not a warning.
Reading AI-generated code
Section titled “Reading AI-generated code”AI assistants frequently reach for if r.is_ok() { r.unwrap() } (or the Option equivalent, if o.is_some() { o.unwrap() }) because it “obviously” cannot panic — but it is doing manually, and worse, what a single pattern match already does atomically and idiomatically.
🐛 Spot the bug
fn print_balance(account: Result<i32, String>) { if account.is_ok() { println!("balance: {}", account.unwrap()); }}Show the answer
This compiles and never panics, but it is strictly worse than the idiomatic version: there is no branch handling the Err case at all (a caller passing an error gets total silence, not even a log line), and it checks the variant twice — once via is_ok(), once implicitly inside unwrap(). The fix is if let Ok(balance) = account { println!("balance: {}", balance); } else { println!("balance unavailable"); }, or, better, changing the function itself to return a Result so the caller decides what “unavailable” should look like instead of the message being buried inside print_balance.