RustArena

Step 1 of 7

panic! or Result?

Programs fail. A user types twelve where you asked for a number, a config file is missing, a customer tries to withdraw more money than they have. Rust splits failures into two kinds and gives each its own tool:

Rust has no exceptions. Whether a function can fail is written in its signature, and the compiler makes sure the caller deals with it.

panic!

You've already met panics: indexing past the end of an array and integer overflow both panic. You can also panic on purpose with the panic! macro, which takes a message like println! does:

fn main() {
    println!("loading level...");
    let level = 7;
    if level > 5 {
        panic!("level {level} does not exist");
    }
    println!("level loaded");
}
loading level...

The program stops at the panic!. The message level 7 does not exist goes to the error output (stderr), along with where the panic happened: panicked at src/main.rs:5:9: (file, line, column). A panic means "the code is wrong": the fix is to change the program, not to try again.

Result

For failures that are part of normal life, a function returns Result<T, E>, an enum from the standard library:

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

T and E stand for types that you choose (module 21 calls them generic type parameters). T is the type of the value on success, E the type of the error. A function returning Result<u32, String> gives back either Ok(some_u32) or Err(some_string). Like Some and None, the variants Ok and Err can be used without writing Result:: in front.

Here's a bank withdrawal that can fail:

fn withdraw(balance: u32, amount: u32) -> Result<u32, String> {
    if amount > balance {
        return Err(format!("cannot withdraw {amount}: balance is only {balance}"));
    }
    Ok(balance - amount)
}

fn main() {
    println!("{:?}", withdraw(100, 30));
    println!("{:?}", withdraw(100, 250));
}
Ok(70)
Err("cannot withdraw 250: balance is only 100")

Piece by piece:

Think about what a panic would mean here: one customer asking for too much would crash the bank's server for everyone. Returning an Err lets the caller show "insufficient funds" and carry on.

Common mistakes

fn main() {
    let n: i32 = "5".parse::<i32>();
    println!("{n}");
}
error[E0308]: mismatched types
  |     let n: i32 = "5".parse::<i32>();
  |            ---   ^^^^^^^^^^^^^^^^^^ expected `i32`, found `Result<i32, ParseIntError>`

The next steps show the ways to get the number out.

Your task

Fill in the blanks so withdraw returns an error when the amount is larger than the balance, and the new balance otherwise. The program should print:

Ok(70)
Err("insufficient funds")

Challenge 2

A sign-up form asks for the user's age. Write fn validate_age(age: i32) -> Result<u32, String>:

Challenge 3

A command-line tool asks "Continue? (yes/no)". Write fn parse_yes_no(answer: &str) -> Result<bool, String>:

Next: Handling a Result with match