Rust: ok_or_else over ok_or, and why
Option::ok_or takes an error value and eagerly evaluates it. Option::ok_or_else takes a closure and lazily builds the error only when the option is None. On the happy path, the closure version skips whatever work would have gone into the error, which matters when the error carries a formatted string.
Most of the time the difference is academic. It shows up when someone writes ok_or(format!(...)) in a hot loop and allocates a string on every successful iteration. I look for this in reviews and swap it.
// Allocates on every call, even when the Option is Some:
fn lookup_bad(cache: &HashMap<String, User>, id: &str) -> Result<User, String> {
cache.get(id).cloned().ok_or(format!("user {id} not found"))
}
// Allocates only on the miss path:
fn lookup_good(cache: &HashMap<String, User>, id: &str) -> Result<User, String> {
cache.get(id).cloned().ok_or_else(|| format!("user {id} not found"))
}
The same distinction exists for Result:
Result::unwrap_orvsunwrap_or_elseResult::map_errvs (always lazy, no _else pair)Option::unwrap_orvsunwrap_or_else
Rule of thumb: if the default is a literal or a cheap Copy value, ok_or is fine. If it is the result of a function call, a format, a Vec allocation, anything like that, prefer the _else form. See also /posts/result-and-option-ergonomics-after-six-months/.