RustArena

Step 1 of 7

An enum for everything that can go wrong

In the last module, errors were String messages. That's quick, but the caller can only read a string, not react to it. Suppose a banking app wants to offer a smaller amount when the balance is too low, but show "contact support" when the account is frozen. With string errors it would have to compare message text, and the day someone fixes a typo in a message, that code silently breaks.

The Rust way is an error enum: one variant per way the operation can fail, each carrying the data that describes it.

#[derive(Debug, PartialEq)]
enum WithdrawError {
    InvalidAmount,
    InsufficientFunds { balance: u64, requested: u64 },
    AccountFrozen,
}

struct Account {
    balance: u64,
    frozen: bool,
}

fn withdraw(account: &mut Account, amount: u64) -> Result<u64, WithdrawError> {
    if account.frozen {
        return Err(WithdrawError::AccountFrozen);
    }
    if amount == 0 {
        return Err(WithdrawError::InvalidAmount);
    }
    if amount > account.balance {
        return Err(WithdrawError::InsufficientFunds { balance: account.balance, requested: amount });
    }
    account.balance -= amount;
    Ok(account.balance)
}

fn main() {
    let mut acct = Account { balance: 100, frozen: false };
    for amount in [30, 0, 500] {
        match withdraw(&mut acct, amount) {
            Ok(left) => println!("withdrew {amount}, {left} left"),
            Err(WithdrawError::InsufficientFunds { balance, requested }) => {
                println!("short by {}, you can take up to {balance}", requested - balance);
            }
            Err(e) => println!("declined: {e:?}"),
        }
    }
    acct.frozen = true;
    println!("{:?}", withdraw(&mut acct, 10));
}
withdrew 30, 70 left
declined: InvalidAmount
short by 430, you can take up to 70
Err(AccountFrozen)

What the enum buys you:

Naming: call the type after the operation (WithdrawError, ParseConfigError) and don't repeat "Error" in every variant: WithdrawError::AccountFrozen, not WithdrawError::AccountFrozenError. Clippy's enum_variant_names lint warns when all variants end with the same word.

Common mistakes

#[derive(Debug)]
enum WithdrawError {
    AccountFrozen,
    InvalidAmount,
}

fn withdraw(frozen: bool) -> Result<u32, WithdrawError> {
    if frozen {
        return Err(AccountFrozen);
    }
    Err(WithdrawError::InvalidAmount)
}

fn main() {
    println!("{:?}", withdraw(true));
}
error[E0425]: cannot find value `AccountFrozen` in this scope

Your task

A username must be 1 to 16 characters long and contain only ASCII letters, digits and _. Write validate_username using the UsernameError enum in the starter. Check in this order:

  1. empty: Err(UsernameError::Empty)
  2. more than 16 characters (count with .chars().count()): Err(UsernameError::TooLong(length))
  3. the first character that's not allowed (c.is_ascii_alphanumeric() || c == '_' tells you if it is): Err(UsernameError::InvalidChar(c))

A valid name returns Ok(()).

Challenge 2

A concert sells at most 6 tickets per order. Write the enum BookingError with these variants (derive Debug and PartialEq):

Then write fn book(available: u32, requested: u32) -> Result<u32, BookingError> that returns the number of tickets left after the order. Check in this order: 0 requested gives ZeroTickets, more than 6 gives OverLimit (with limit: 6), more than available gives SoldOut.

Challenge 3

Write a password checker from scratch. Define PasswordError (derive Debug and PartialEq) with the variants TooShort(usize), NoDigit, NoUppercase and ContainsUsername, and write fn check_password(password: &str, username: &str) -> Result<(), PasswordError>. Check in this order:

  1. fewer than 8 characters: TooShort(<number of characters>)
  2. no ASCII digit (c.is_ascii_digit()): NoDigit
  3. no uppercase letter (c.is_uppercase()): NoUppercase
  4. the password contains the username, ignoring case: ContainsUsername

Next: Display: messages for people