ExamplesBy LevelBy TopicLearning Paths
1008 Intermediate

1008-option-to-result — Option to Result

Functional Programming

Tutorial

The Problem

Option<T> represents the presence or absence of a value, while Result<T, E> represents success or a specific failure reason. Real programs often start with an Option lookup — for example, reading a key from a map — and need to convert it into a Result that can carry an error message and propagate through the ? operator.

Rust provides two conversion methods: ok_or (eager, always constructs the error value) and ok_or_else (lazy, only constructs the error if None is encountered). This distinction matters for performance when the error value is expensive to build.

🎯 Learning Outcomes

  • • Understand when to use Option versus Result for absent values
  • • Convert Option<T> to Result<T, E> using ok_or and ok_or_else
  • • Chain Option-to-Result conversions inside ?-based pipelines
  • • Recognise the cost difference between eager and lazy error construction
  • • Go the other direction: Result::ok() to collapse Result back to Option
  • Code Example

    #![allow(clippy::all)]
    // 1008: Option to Result Conversion
    // Convert Option<T> to Result<T, E> with ok_or / ok_or_else
    
    use std::collections::HashMap;
    
    fn build_users() -> HashMap<String, (String, u32)> {
        let mut m = HashMap::new();
        m.insert("Alice".into(), ("alice@ex.com".into(), 30));
        m.insert("Bob".into(), ("bob@ex.com".into(), 17));
        m
    }
    
    // Approach 1: ok_or — eager error value
    fn find_user_eager<'a>(
        users: &'a HashMap<String, (String, u32)>,
        name: &str,
    ) -> Result<&'a (String, u32), String> {
        users.get(name).ok_or(format!("user not found: {}", name))
    }
    
    // Approach 2: ok_or_else — lazy error (avoids allocation if Some)
    fn find_user_lazy<'a>(
        users: &'a HashMap<String, (String, u32)>,
        name: &str,
    ) -> Result<&'a (String, u32), String> {
        users
            .get(name)
            .ok_or_else(|| format!("user not found: {}", name))
    }
    
    // Approach 3: Chaining Option->Result in a pipeline
    fn find_and_validate(
        users: &HashMap<String, (String, u32)>,
        name: &str,
        min_age: u32,
    ) -> Result<(String, u32), String> {
        users
            .get(name)
            .ok_or_else(|| format!("user not found: {}", name))
            .and_then(|(email, age)| {
                if *age >= min_age {
                    Ok((email.clone(), *age))
                } else {
                    Err(format!("{} is too young ({} < {})", name, age, min_age))
                }
            })
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_ok_or_found() {
            let users = build_users();
            let result = find_user_eager(&users, "Alice");
            assert!(result.is_ok());
            assert_eq!(result.unwrap().1, 30);
        }
    
        #[test]
        fn test_ok_or_not_found() {
            let users = build_users();
            let result = find_user_eager(&users, "Unknown");
            assert_eq!(result.unwrap_err(), "user not found: Unknown");
        }
    
        #[test]
        fn test_ok_or_else_lazy() {
            let users = build_users();
            assert!(find_user_lazy(&users, "Bob").is_ok());
            assert!(find_user_lazy(&users, "Nobody").is_err());
        }
    
        #[test]
        fn test_validate_success() {
            let users = build_users();
            let result = find_and_validate(&users, "Alice", 18);
            assert_eq!(result.unwrap(), ("alice@ex.com".into(), 30));
        }
    
        #[test]
        fn test_validate_too_young() {
            let users = build_users();
            let result = find_and_validate(&users, "Bob", 18);
            assert!(result.unwrap_err().contains("too young"));
        }
    
        #[test]
        fn test_validate_not_found() {
            let users = build_users();
            let result = find_and_validate(&users, "Nobody", 18);
            assert!(result.unwrap_err().contains("not found"));
        }
    
        #[test]
        fn test_option_methods() {
            // Direct Option -> Result conversions
            assert_eq!(Some(42).ok_or("missing"), Ok(42));
            assert_eq!(None::<i32>.ok_or("missing"), Err("missing"));
    
            // Result -> Option conversions
            assert_eq!(Ok::<i32, &str>(42).ok(), Some(42));
            assert_eq!(Err::<i32, &str>("fail").ok(), None);
        }
    }

    Key Differences

  • Built-in conversion methods: Rust has ok_or / ok_or_else as inherent methods; OCaml requires manual matching or a library helper.
  • Lazy vs eager: Both languages can express lazy error construction, but Rust makes the distinction explicit with two differently named methods.
  • **? integration**: Rust's ? can be applied directly to Result after conversion; OCaml's equivalent (let*) requires the result to already be in the monad.
  • Null safety: Neither language has null; both use sum types for optional values, ensuring exhaustiveness at compile time.
  • OCaml Approach

    OCaml has no ok_or method on Option, but the pattern is one line:

    let option_to_result opt msg =
      match opt with
      | Some v -> Ok v
      | None -> Error msg
    

    Libraries like Base provide Option.value_exn and Option.to_or_error. The lazy variant is expressed with a thunk: None -> Error (msg ()).

    Full Source

    #![allow(clippy::all)]
    // 1008: Option to Result Conversion
    // Convert Option<T> to Result<T, E> with ok_or / ok_or_else
    
    use std::collections::HashMap;
    
    fn build_users() -> HashMap<String, (String, u32)> {
        let mut m = HashMap::new();
        m.insert("Alice".into(), ("alice@ex.com".into(), 30));
        m.insert("Bob".into(), ("bob@ex.com".into(), 17));
        m
    }
    
    // Approach 1: ok_or — eager error value
    fn find_user_eager<'a>(
        users: &'a HashMap<String, (String, u32)>,
        name: &str,
    ) -> Result<&'a (String, u32), String> {
        users.get(name).ok_or(format!("user not found: {}", name))
    }
    
    // Approach 2: ok_or_else — lazy error (avoids allocation if Some)
    fn find_user_lazy<'a>(
        users: &'a HashMap<String, (String, u32)>,
        name: &str,
    ) -> Result<&'a (String, u32), String> {
        users
            .get(name)
            .ok_or_else(|| format!("user not found: {}", name))
    }
    
    // Approach 3: Chaining Option->Result in a pipeline
    fn find_and_validate(
        users: &HashMap<String, (String, u32)>,
        name: &str,
        min_age: u32,
    ) -> Result<(String, u32), String> {
        users
            .get(name)
            .ok_or_else(|| format!("user not found: {}", name))
            .and_then(|(email, age)| {
                if *age >= min_age {
                    Ok((email.clone(), *age))
                } else {
                    Err(format!("{} is too young ({} < {})", name, age, min_age))
                }
            })
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_ok_or_found() {
            let users = build_users();
            let result = find_user_eager(&users, "Alice");
            assert!(result.is_ok());
            assert_eq!(result.unwrap().1, 30);
        }
    
        #[test]
        fn test_ok_or_not_found() {
            let users = build_users();
            let result = find_user_eager(&users, "Unknown");
            assert_eq!(result.unwrap_err(), "user not found: Unknown");
        }
    
        #[test]
        fn test_ok_or_else_lazy() {
            let users = build_users();
            assert!(find_user_lazy(&users, "Bob").is_ok());
            assert!(find_user_lazy(&users, "Nobody").is_err());
        }
    
        #[test]
        fn test_validate_success() {
            let users = build_users();
            let result = find_and_validate(&users, "Alice", 18);
            assert_eq!(result.unwrap(), ("alice@ex.com".into(), 30));
        }
    
        #[test]
        fn test_validate_too_young() {
            let users = build_users();
            let result = find_and_validate(&users, "Bob", 18);
            assert!(result.unwrap_err().contains("too young"));
        }
    
        #[test]
        fn test_validate_not_found() {
            let users = build_users();
            let result = find_and_validate(&users, "Nobody", 18);
            assert!(result.unwrap_err().contains("not found"));
        }
    
        #[test]
        fn test_option_methods() {
            // Direct Option -> Result conversions
            assert_eq!(Some(42).ok_or("missing"), Ok(42));
            assert_eq!(None::<i32>.ok_or("missing"), Err("missing"));
    
            // Result -> Option conversions
            assert_eq!(Ok::<i32, &str>(42).ok(), Some(42));
            assert_eq!(Err::<i32, &str>("fail").ok(), None);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_ok_or_found() {
            let users = build_users();
            let result = find_user_eager(&users, "Alice");
            assert!(result.is_ok());
            assert_eq!(result.unwrap().1, 30);
        }
    
        #[test]
        fn test_ok_or_not_found() {
            let users = build_users();
            let result = find_user_eager(&users, "Unknown");
            assert_eq!(result.unwrap_err(), "user not found: Unknown");
        }
    
        #[test]
        fn test_ok_or_else_lazy() {
            let users = build_users();
            assert!(find_user_lazy(&users, "Bob").is_ok());
            assert!(find_user_lazy(&users, "Nobody").is_err());
        }
    
        #[test]
        fn test_validate_success() {
            let users = build_users();
            let result = find_and_validate(&users, "Alice", 18);
            assert_eq!(result.unwrap(), ("alice@ex.com".into(), 30));
        }
    
        #[test]
        fn test_validate_too_young() {
            let users = build_users();
            let result = find_and_validate(&users, "Bob", 18);
            assert!(result.unwrap_err().contains("too young"));
        }
    
        #[test]
        fn test_validate_not_found() {
            let users = build_users();
            let result = find_and_validate(&users, "Nobody", 18);
            assert!(result.unwrap_err().contains("not found"));
        }
    
        #[test]
        fn test_option_methods() {
            // Direct Option -> Result conversions
            assert_eq!(Some(42).ok_or("missing"), Ok(42));
            assert_eq!(None::<i32>.ok_or("missing"), Err("missing"));
    
            // Result -> Option conversions
            assert_eq!(Ok::<i32, &str>(42).ok(), Some(42));
            assert_eq!(Err::<i32, &str>("fail").ok(), None);
        }
    }

    Deep Comparison

    Option to Result — Comparison

    Core Insight

    Both languages need to bridge the gap between "absence" (Option/None) and "error" (Result/Error). The conversion adds semantic meaning to what was just a missing value.

    OCaml Approach

  • • Pattern match on option and return Ok/Error
  • • Custom ok_or and ok_or_else helpers (not in stdlib before 4.08)
  • • Lazy version uses fun () -> error_value thunk
  • Rust Approach

  • • Built-in Option::ok_or(err) and Option::ok_or_else(|| err)
  • • Reverse: Result::ok() and Result::err() to get Options
  • • Chains naturally: .ok_or_else(|| ...)?.and_then(...)
  • Comparison Table

    AspectOCamlRust
    Option to ResultCustom helper or match.ok_or() / .ok_or_else()
    Result to OptionCustom helper.ok() / .err()
    Lazy errorfun () -> ... thunk\|\| ... closure
    Chaining\|> pipeline.method() chain
    In stdlibSince 4.08 (partial)Always available

    Exercises

  • Add a find_user_by_email function that searches the map by email value instead of key, returning Option<&str> converted to Result.
  • Write a function that chains find_and_validate for multiple names, collecting all successful results into a Vec.
  • Implement the reverse direction: a function that takes a Result<User, String> and maps it to Option<User>, discarding the error reason.
  • Open Source Repos