306: ok_or and ok_or_else
Tutorial Video
Text description (accessibility)
This video demonstrates the "306: ok_or and ok_or_else" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Code often needs to convert an `Option<T>` to a `Result<T, E>` — treating absence as an error with a specific message. Key difference from OCaml: 1. **Naming**: `ok_or` maps `None
Tutorial
The Problem
Code often needs to convert an Option<T> to a Result<T, E> — treating absence as an error with a specific message. A missing configuration key is an Option<&str>, but the caller expects a Result with a descriptive error. The ok_or() and ok_or_else() methods convert Option<T> to Result<T, E>, supplying an error value for the None case. This is the bridge between optionality and error handling.
🎯 Learning Outcomes
ok_or(err) to convert None to Err(err) — eager error valueok_or_else(|| err) to convert None to Err(f()) — lazy error computationok_or_else with ? to propagate missing values as typed errorsok_or_else → ? for "required key" semanticsCode Example
#![allow(clippy::all)]
//! # ok_or and ok_or_else
//!
//! `ok_or(err)` converts `Option<T>` to `Result<T, E>`.
use std::collections::HashMap;
/// Lookup with descriptive error
pub fn lookup<'a>(map: &'a HashMap<&str, &str>, key: &str) -> Result<&'a str, String> {
map.get(key)
.copied()
.ok_or_else(|| format!("key '{}' not found", key))
}
/// Get port from config
pub fn get_port(config: &HashMap<&str, &str>) -> Result<u16, String> {
let s = config.get("port").copied().ok_or("port not set")?;
s.parse::<u16>().map_err(|e| format!("invalid port: {}", e))
}
/// Convert Option to Result with eager error
pub fn require_some<T>(opt: Option<T>, err: &str) -> Result<T, String> {
opt.ok_or_else(|| err.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ok_or_some() {
assert_eq!(Some(5i32).ok_or("missing"), Ok(5));
}
#[test]
fn test_ok_or_none() {
assert_eq!(None::<i32>.ok_or("missing"), Err("missing"));
}
#[test]
fn test_ok_or_else_lazy() {
let mut called = false;
let _: Result<i32, &str> = Some(5).ok_or_else(|| {
called = true;
"err"
});
assert!(!called);
}
#[test]
fn test_lookup_found() {
let mut map = HashMap::new();
map.insert("host", "localhost");
assert_eq!(lookup(&map, "host").unwrap(), "localhost");
}
#[test]
fn test_lookup_missing() {
let map: HashMap<&str, &str> = HashMap::new();
assert!(lookup(&map, "missing").is_err());
}
#[test]
fn test_get_port() {
let mut map = HashMap::new();
map.insert("port", "8080");
assert_eq!(get_port(&map).unwrap(), 8080);
}
}Key Differences
ok_or maps None -> Err(e), Some(v) -> Ok(v) — the name reads as "convert to Ok, or use this error".ok_or(err) always evaluates err (even for Some); ok_or_else(|| err) is lazy — prefer ok_or_else for format strings or allocations.ok_or is a standard method on Option in Rust; OCaml's Option.to_result is in the Base library or requires manual wrapping.Result::ok() converts Result<T, E> to Option<T> — ok_or and ok() are inverses (with loss of error detail in the ok() direction).OCaml Approach
OCaml uses Option.to_result ~none:err (Base library) or Option.fold:
let lookup map key =
Hashtbl.find_opt map key
|> Option.to_result ~none:(Printf.sprintf "key '%s' not found" key)
(* Or manually: *)
let require_some msg = function
| None -> Error msg
| Some v -> Ok v
Full Source
#![allow(clippy::all)]
//! # ok_or and ok_or_else
//!
//! `ok_or(err)` converts `Option<T>` to `Result<T, E>`.
use std::collections::HashMap;
/// Lookup with descriptive error
pub fn lookup<'a>(map: &'a HashMap<&str, &str>, key: &str) -> Result<&'a str, String> {
map.get(key)
.copied()
.ok_or_else(|| format!("key '{}' not found", key))
}
/// Get port from config
pub fn get_port(config: &HashMap<&str, &str>) -> Result<u16, String> {
let s = config.get("port").copied().ok_or("port not set")?;
s.parse::<u16>().map_err(|e| format!("invalid port: {}", e))
}
/// Convert Option to Result with eager error
pub fn require_some<T>(opt: Option<T>, err: &str) -> Result<T, String> {
opt.ok_or_else(|| err.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ok_or_some() {
assert_eq!(Some(5i32).ok_or("missing"), Ok(5));
}
#[test]
fn test_ok_or_none() {
assert_eq!(None::<i32>.ok_or("missing"), Err("missing"));
}
#[test]
fn test_ok_or_else_lazy() {
let mut called = false;
let _: Result<i32, &str> = Some(5).ok_or_else(|| {
called = true;
"err"
});
assert!(!called);
}
#[test]
fn test_lookup_found() {
let mut map = HashMap::new();
map.insert("host", "localhost");
assert_eq!(lookup(&map, "host").unwrap(), "localhost");
}
#[test]
fn test_lookup_missing() {
let map: HashMap<&str, &str> = HashMap::new();
assert!(lookup(&map, "missing").is_err());
}
#[test]
fn test_get_port() {
let mut map = HashMap::new();
map.insert("port", "8080");
assert_eq!(get_port(&map).unwrap(), 8080);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ok_or_some() {
assert_eq!(Some(5i32).ok_or("missing"), Ok(5));
}
#[test]
fn test_ok_or_none() {
assert_eq!(None::<i32>.ok_or("missing"), Err("missing"));
}
#[test]
fn test_ok_or_else_lazy() {
let mut called = false;
let _: Result<i32, &str> = Some(5).ok_or_else(|| {
called = true;
"err"
});
assert!(!called);
}
#[test]
fn test_lookup_found() {
let mut map = HashMap::new();
map.insert("host", "localhost");
assert_eq!(lookup(&map, "host").unwrap(), "localhost");
}
#[test]
fn test_lookup_missing() {
let map: HashMap<&str, &str> = HashMap::new();
assert!(lookup(&map, "missing").is_err());
}
#[test]
fn test_get_port() {
let mut map = HashMap::new();
map.insert("port", "8080");
assert_eq!(get_port(&map).unwrap(), 8080);
}
}
Deep Comparison
ok-or-patterns
See README.md for details.
Exercises
HashMap<String, String> config, using ok_or_else to produce descriptive errors for each missing required key.required<T: Clone>(map: &HashMap<&str, T>, key: &str) -> Result<T, String> helper that extracts required map values.ok_or_else calls with ? to look up host, port, and path from a config map, failing with a descriptive error if any is missing.