ExamplesBy LevelBy TopicLearning Paths
306 Intermediate

306: ok_or and ok_or_else

Functional Programming

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

  • • Use ok_or(err) to convert None to Err(err) — eager error value
  • • Use ok_or_else(|| err) to convert None to Err(f()) — lazy error computation
  • • Chain ok_or_else with ? to propagate missing values as typed errors
  • • Recognize the pattern: HashMap lookup → ok_or_else? for "required key" semantics
  • Code 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

  • Naming: ok_or maps None -> Err(e), Some(v) -> Ok(v) — the name reads as "convert to Ok, or use this error".
  • Eagerness: ok_or(err) always evaluates err (even for Some); ok_or_else(|| err) is lazy — prefer ok_or_else for format strings or allocations.
  • Standard library: ok_or is a standard method on Option in Rust; OCaml's Option.to_result is in the Base library or requires manual wrapping.
  • Inverse: 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);
        }
    }
    ✓ Tests Rust test suite
    #[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

  • Write a function that validates a HashMap<String, String> config, using ok_or_else to produce descriptive errors for each missing required key.
  • Implement a required<T: Clone>(map: &HashMap<&str, T>, key: &str) -> Result<T, String> helper that extracts required map values.
  • Chain three 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.
  • Open Source Repos