Chapter 11

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

The snippet above sneaks in three pieces of syntax that don't have their own chapter, so it's worth pausing on each:

&'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 in general get a more careful treatment in later chapters.

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

Back to Result

The &'static str you see for the error type is the simplest possible error: a borrowed string literal. Real applications usually define their own error enums, but &'static str is fine while you're learning.

Patterns to handle a Result:

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. Once you're comfortable with this chapter, the ? operator (chapter 19) will let you chain fallible operations without the boilerplate.

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 is the bounds check. Floating-point comparison has plenty of nasty edge cases in general, but exact zero is fine.
  • 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
    
                

    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
      
                  

      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. Chapter 12 makes this explicit; for now, just notice the function compiles even though no lifetimes appear 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
        
                    

        Parse percentage

        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.

        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
          
                      

          Wrapping up `Result`

          You produced Results with simple if checks, returned an owned String in the Ok arm, borrowed from the input via lifetime elision, and combined strip_suffix, parse, and a bounds check into a real validating parser.

          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 the cheapest error type: a borrowed string literal that lives forever. Real applications usually graduate to enums or String-based errors, but this is a fine starting point.
          • 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 (chapter 19) will let you chain fallible calls without writing match every time. For now, match is fine.
          Next chapter 12Ownership and Borrowing