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"),
    }
}

There are three bits of syntax here you may not have seen yet: &'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. We'll cover lifetimes in more detail 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. I prefer the guard here because it keeps the condition next to the pattern.

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

You can produce a Result with an if that checks the failure case and an else branch that returns Ok(...).

The error type is &'static str, so you can return a fixed error message without defining a new type.

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.
  • divisor == 0.0 detects both positive and negative zero, the failure cases for this exercise.
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_eq!(safe_divide(10.0, 0.0), Err("cannot divide by zero"));
        assert_eq!(safe_divide(10.0, -0.0), Err("cannot divide by zero"));
    }
    

    Read Config File

    This simulates reading a file: return an error for an empty filename and the fixed content for any other name. It uses the same pattern as safe_divide, but returns 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 checks whether the filename is empty.
    • 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_eq!(read_config_file(""), Err("File not found"));
      }
      

      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. The memory and ownership chapter explains why a returned reference must not outlive the value it borrows.

      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. You don't need to allocate a String with to_string().
      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:

        So "255%" is out of range, while "256" is a parse error for this exercise. Only one trailing % is allowed.

        The error type here is &'static str, so use string literals for the messages. 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 tries to parse the text and returns a Result. The turbofish (parse::<u8>()) tells it which numeric type to produce. u8 already rejects negative numbers and anything above 255, so those inputs produce parse errors.
        • Result::map_err transforms the error value without touching Ok. You can use it 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. If the text cannot be parsed as u8, returns
          /// `Err("not a valid percentage")`. A parsed value above 100 returns
          /// `Err("percentage must be between 0 and 100")`. Only one trailing `%` is
          /// allowed.
          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_eq!(
                  parse_percentage("101"),
                  Err("percentage must be between 0 and 100")
              );
              assert_eq!(
                  parse_percentage("255%"),
                  Err("percentage must be between 0 and 100")
              );
              // These cannot be parsed as u8, unlike 101 and 255 above.
              assert_eq!(parse_percentage("256"), Err("not a valid percentage"));
              assert_eq!(parse_percentage("-1"), Err("not a valid percentage"));
              assert_eq!(parse_percentage("half"), Err("not a valid percentage"));
              assert_eq!(parse_percentage(""), Err("not a valid percentage"));
              assert_eq!(parse_percentage("75%%"), Err("not a valid percentage"));
          }
          

          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

          Optional Chapters

          Extra practice to explore at your own pace. These chapters do not count toward course progress.