unwrap_or(fallback) looks lazy, like it only runs fallback when there's an Err to fall back to. It doesn't — fallback is an ordinary argument, and Rust evaluates every argument before the call happens, regardless of which branch needs it. If building that fallback value can fail on its own, it fails every single time, even on the Ok path that never needed it.
fn risky_fallback() -> i32 {
let z = std::env::args().count() as i32 - 1; // 0 when run with no extra args
10 / z
}
let result = safe_divide(10, 2); // Ok(5)
let value = result.unwrap_or(risky_fallback()); // called unconditionally -- even though result is Ok
println!("got: {}", value);Rust has no exceptions. Instead, a function that can fail returns Result<T, E>, an enum with Ok(T) for success and Err(E) for failure. Because it's an ordinary type, the compiler nudges the caller to handle both cases: Result is marked #[must_use], so calling a Result-returning function and dropping the value produces an "unused Result that must be used" warning (not an error — the code still compiles and runs, silently discarding an Err at runtime if there is one). That nudge can also be switched off entirely and on purpose: writing let _ = might_fail(); discards the Result with no warning at all. So it's not literally impossible to silently ignore a possible error the way it's impossible to, say, use a moved value — it's a lint you're expected to heed, not a wall you can't get past.
Result<T, E> and match
The standard library defines Result as an ordinary two-variant enum, handled the same way as any other enum — typically with match.
// enum Result<T, E> { Ok(T), Err(E) }
use std::fs::File;
let f = File::open("hello.txt");
match f {
Ok(file) => println!("opened: {:?}", file),
Err(error) => println!("failed: {:?}", error),
}The ? operator: one character for error propagation
? after a Result-returning expression unwraps Ok automatically, or returns early with the Err — converting it via From if the error types differ. This replaces what would otherwise be a repetitive match-and-early-return in every fallible function.
use std::fs::File;
use std::io::{self, Read};
fn read_username() -> Result<String, io::Error> {
let mut f = File::open("username.txt")?; // returns early on Err
let mut s = String::new();
f.read_to_string(&mut s)?; // same here
Ok(s)
}unwrap, expect, and when they're acceptable
unwrap() panics immediately on Err, extracting the Ok value or crashing; expect("message") does the same with a custom panic message. Idiomatic Rust reserves these for cases where failure genuinely means a bug (prototypes, tests, or an invariant that truly cannot fail) rather than production error handling, where ? or an explicit match is preferred.
let f = File::open("hello.txt").unwrap(); // panics if the file is missing
let f = File::open("hello.txt").expect("hello.txt should exist"); // panics with a custom messageThat eagerness is exactly what trips up the bug above: result.unwrap_or(risky_fallback()) calls risky_fallback() no matter what result is, so a fallback that can itself panic takes the whole program down even when result was Ok. unwrap_or_else fixes this by taking a closure instead of a value — the closure is only ever invoked if the Result is actually an Err:
fn risky_fallback() -> i32 {
let z = std::env::args().count() as i32 - 1;
10 / z
}
let result = safe_divide(10, 2); // Ok(5)
let value = result.unwrap_or_else(|_| risky_fallback()); // closure -- only called on Err
println!("got: {}", value); // got: 5 -- risky_fallback never runs