Chapter 13

Result<T, E>: When an operation might fail

👋 Anyone can read and edit this exercise. Sign up to save your progress.

Failure is not an Option<T>, but a Result<T, E>.

Result is the sibling of Option. Where Option says "maybe a value, maybe nothing", Result says "maybe a value, maybe an error":

enum Result<T, E> {
    Ok(T),
    Err(E),
}

This is how Rust handles fallible operations. There are no exceptions. A function that can fail says so in its signature, and the caller has to deal with both branches.

fn parse_port(input: &str) -> Result<u16, &'static str> {
    match input.parse::<u16>() {
        Ok(n) if n > 0 => Ok(n),
        Ok(_) => Err("port must be greater than 0"),
        Err(_) => Err("not a valid number"),
    }
}

You only need a working reading of three details in this example: &'static str, the ::<u16> after parse, and the if guard on the first match arm.

&'static str

This is a &str whose lifetime is 'static: a fancy way of saying "this string lives for the entire duration of the program." String literals like "port must be greater than 0" are baked into the binary, so they qualify. For now, treat &'static str as the right type to use for hard-coded error messages. Lifetimes receive more detailed treatment later.

Turbofish: parse::<u16>()

.parse() doesn't know which type you want to parse into, so you tell it with the funny-looking ::<T> syntax. It's just a way to spell out a generic type argument at the call site:

let n = "42".parse::<u16>().unwrap();
// equivalent if the type can be inferred from context:
let n: u16 = "42".parse().unwrap();

You'll see this anywhere a function returns T and the type isn't clear from the surrounding code.

Match guards: Ok(n) if n > 0 => ...

The if n > 0 clause on a match arm is called a guard. The arm only fires when both the pattern matches and the guard is true. Without it you'd need a nested if inside the arm body, which reads worse.

match n {
    x if x < 0 => "negative",
    0          => "zero",
    _          => "positive",
}

Handling the two variants

As with Option, you can handle a Result with match, if let, or a combinator when you have a simple fallback:

match safe_divide(10.0, 0.0) {
    Ok(n) => println!("got {n}"),
    Err(e) => println!("oops: {e}"),
}

let n = safe_divide(10.0, 2.0).unwrap_or(0.0);

if let Ok(n) = safe_divide(10.0, 2.0) {
    println!("got {n}");
}

Result has many of the same combinators as Option: .map, .map_or, .and_then, .unwrap_or. The ? operator chains fallible operations without repeating the same match boilerplate each time.

Safe divide

The simplest way to produce a Result: an if checks the failure case, and the else branch returns Ok(...).

The signature is the interesting part: &'static str for the error is the simplest possible error type and is fine while you're learning.

Useful from the standard library

  • The Result constructors Ok(value) and Err(message) are in the prelude, so you can use them without importing anything.
  • f64 == 0.0 detects the failure case. Floating-point comparison has plenty of nasty edge cases in general, but checking for exact zero is fine here.
  • Result::is_err is what the test uses; you don't need it inside the function.
Exercise 1 of 4
Open in Web Editor

Results

    Compiler / runtime output
    
                
    Reveal the full solution Spoiler: the complete answer
    /// Divides `dividend` by `divisor`.
    ///
    /// Returns `Ok(quotient)` when the division is well-defined, or
    /// `Err("cannot divide by zero")` when `divisor` is `0.0`.
    ///
    /// Start here. The simplest way to produce a `Result`: an `if` checks
    /// the failure case, the `else` branch returns `Ok(...)`.
    ///
    /// The signature is the interesting part: `&'static str` for the error
    /// is the simplest possible error type and is fine while you're learning.
    fn safe_divide(dividend: f64, divisor: f64) -> Result<f64, &'static str> {
        if divisor == 0.0 {
            Err("cannot divide by zero")
        } else {
            Ok(dividend / divisor)
        }
    }
    
    #[test]
    fn test_safe_divide() {
        assert_eq!(safe_divide(10.0, 2.0), Ok(5.0));
        assert_eq!(safe_divide(-9.0, 3.0), Ok(-3.0));
        assert!(safe_divide(10.0, 0.0).is_err());
    }
    

    Read config file

    Same idea as safe_divide, but returning an owned String. Notice you can mix Ok(String::from("...")) and Err("...") in the same function: the success and error types are independent.

    Useful from the standard library

    • str::is_empty is the cleanest way to detect an empty filename.
    • String::from or .to_string() turns the literal "config content" into the owned String the Ok arm needs.
    Exercise 2 of 4
    Open in Web Editor

    Results

      Compiler / runtime output
      
                  
      Reveal the full solution Spoiler: the complete answer
      /// Reads a configuration file (simulated).
      /// Returns Ok(content) normally, Err("File not found") for empty input.
      ///
      /// Same idea as `safe_divide`, but returning an owned `String`. Notice
      /// you can mix `Ok(String::from("..."))` and `Err("...")` in the same
      /// function: the success and error types are independent.
      fn read_config_file(filename: &str) -> Result<String, &'static str> {
          if filename.is_empty() {
              Err("File not found")
          } else {
              Ok(String::from("config content"))
          }
      }
      
      #[test]
      fn test_read_config_file() {
          assert_eq!(
              read_config_file("app.toml"),
              Ok("config content".to_string())
          );
          assert!(read_config_file("").is_err());
      }
      

      Validate email

      Now the Ok value is a borrow of the input. The &str in the return type implicitly borrows from email, so the compiler infers a lifetime linking input and output via lifetime elision. That lifetime relationship becomes explicit in the memory and ownership material. For now, notice that the function compiles even though no lifetime appears in the signature.

      Useful from the standard library

      • str::contains takes a char (or another &str) and answers yes/no. So email.contains('@') is exactly the check you need.
      • The Ok branch can return the input slice directly: it's already a &str with the right lifetime. No to_string() allocation needed.
      Exercise 3 of 4
      Open in Web Editor

      Results

        Compiler / runtime output
        
                    
        Reveal the full solution Spoiler: the complete answer
        /// Validates an email address (basic check).
        /// Returns Ok(email) if contains '@', Err(message) otherwise.
        ///
        /// Now the `Ok` value is a borrow of the input. The `&str` in the return
        /// type implicitly borrows from `email`, so the compiler infers a lifetime
        /// linking input and output via lifetime elision. Chapter 12 makes this
        /// explicit; for now, just notice the function compiles even though no
        /// lifetimes appear in the signature.
        fn validate_email(email: &str) -> Result<&str, &'static str> {
            if email.contains('@') {
                Ok(email)
            } else {
                Err("email must contain '@'")
            }
        }
        
        #[test]
        fn test_validate_email() {
            assert_eq!(validate_email("user@example.com"), Ok("user@example.com"));
            assert!(validate_email("invalid-email").is_err());
        }
        

        Parse percentage

        A trailing % is allowed, but the remaining text may still fail to parse, and a parsed u8 may be greater than 100. Those two failures need different error messages.

        Note: the error type here is &'static str, which means the message has to be a string literal. If you find yourself wanting format!("{input} is out of range") in an Err, you'd need to change the return type to Result<u8, String>. Stick with literals for this exercise.

        Useful from the standard library

        • str::strip_suffix removes a trailing pattern if present and returns Option<&str>. input.strip_suffix('%').unwrap_or(input) peels the % when there is one.
        • str::parse is the canonical "may fail to parse" call. The turbofish (parse::<u8>()) tells it which numeric type to produce. u8 already rejects negative numbers and anything above 255, which catches a couple of cases for free.
        • Result::map_err swaps the error type without touching Ok. Handy to turn the parser's error into your own static message.
        • A bounds check if n > 100 { return Err("...") } finishes the job; the u8 type already takes care of n < 0.
        Exercise 4 of 4
        Open in Web Editor

        Results

          Compiler / runtime output
          
                      
          Reveal the full solution Spoiler: the complete answer
          /// Parses a percentage from a string. Accepts integers in `0..=100`,
          /// optionally with a trailing `%` (so `"42"` and `"42%"` both work).
          ///
          /// Returns `Ok(value)` on success, `Err(message)` otherwise.
          ///
          /// This is the hardest function in the chapter; the previous three
          /// were warmups. More than one thing can go wrong, and they need
          /// different error messages. Strip the optional `%` first, then
          /// `parse::<u8>()` the rest, then bounds-check. Each step is its own
          /// potential `Err`.
          ///
          /// Note: the error type here is `&'static str`, which means the message
          /// has to be a string literal. If you find yourself wanting
          /// `format!("{input} is out of range")` in an `Err`, you'd need to
          /// change the return type to `Result<u8, String>`. Stick with literals
          /// for this exercise.
          fn parse_percentage(input: &str) -> Result<u8, &'static str> {
              let digits = input.strip_suffix('%').unwrap_or(input);
              match digits.parse::<u8>() {
                  Ok(value) if value <= 100 => Ok(value),
                  Ok(_) => Err("percentage must be between 0 and 100"),
                  Err(_) => Err("not a valid percentage"),
              }
          }
          
          #[test]
          fn test_parse_percentage() {
              assert_eq!(parse_percentage("0"), Ok(0));
              assert_eq!(parse_percentage("42"), Ok(42));
              assert_eq!(parse_percentage("100"), Ok(100));
              assert_eq!(parse_percentage("75%"), Ok(75));
              assert!(parse_percentage("101").is_err());
              assert!(parse_percentage("-1").is_err());
              assert!(parse_percentage("half").is_err());
              assert!(parse_percentage("").is_err());
          }
          

          Wrapping up `Result`

          The examples use simple if checks to build Results with owned and borrowed success values. Combining strip_suffix, parse, and a bounds check produces a validating parser with several failure cases.

          What we learned

          • Result<T, E> is how Rust expresses fallibility. There are no exceptions; a function that can fail says so in its signature.
          • Ok(value) and Err(error) are the constructors. Both are in the prelude.
          • Result has the same combinator family as Option: unwrap_or, map, map_or, plus map_err for transforming the error side and ok to drop the error and convert to Option<T>.
          • &'static str is a convenient error type when every message is a fixed string literal. Applications often use error enums or owned Strings once errors need data of their own.
          • The turbofish (parse::<u8>()) spells out a generic type argument at the call site when the type isn't clear from context.
          • Match guards (Ok(n) if n > 0 => ...) attach a boolean condition to a pattern. The arm only fires when both hold.
          • The ? operator chains fallible calls without requiring a match each time. For now, match keeps both paths visible.
          Next chapter 14The `?` operator