RustArena

Step 6 of 7

panic or Result? The guidelines

You now have every tool. The hard part at work is choosing. These are the guidelines most Rust teams follow (they come from the Rust book and the Rust API guidelines):

Situation Use
Bad user input, missing file, network timeout, invalid data Result
A business rule says no (insufficient funds, sold out) Result
A bug: a broken invariant, a state the code assumes is impossible panic! (often through expect, assert! or unreachable!)
Tests, examples, quick prototypes unwrap/expect are fine
Library functions Result, and panic only when the caller broke a documented rule

The key question: can this happen when the program is correct? A user typing abc happens in a correct program, so it's a Result. An index that your own code computed being out of range only happens if your code is wrong, so a panic (which points straight at the bug) is right.

Offer both, like the standard library

The standard library often provides a panicking version and a checking version, so the caller picks:

Panics on failure Returns Option
v[i] v.get(i)
a / b (b is 0) a.checked_div(b)
a + b (overflow, debug build) a.checked_add(b)

If a function can panic, its documentation says so under a # Panics heading, so callers know the rule they must follow:

/// Returns the average of the scores.
///
/// # Panics
///
/// Panics if `scores` is empty.
fn average(scores: &[u32]) -> u32 {
    assert!(!scores.is_empty(), "average of no scores");
    let mut total = 0;
    for score in scores {
        total += score;
    }
    total / scores.len() as u32
}

assert!(condition, "message") panics with the message when the condition is false. It's the usual way to check a rule the caller must follow.

Validate once, then trust the type

A useful pattern: check a value once, when it's created, and wrap it in a type that can only hold valid values. After that, the rest of the code needs no checks and no Results.

#[derive(Debug, PartialEq)]
enum PercentError {
    TooLarge(u32),
}

struct Percent(u32);

impl Percent {
    fn new(value: u32) -> Result<Percent, PercentError> {
        if value > 100 {
            return Err(PercentError::TooLarge(value));
        }
        Ok(Percent(value))
    }

    fn of(&self, amount: u32) -> u32 {
        amount * self.0 / 100
    }
}

fn main() {
    let discount = Percent::new(25).expect("25 is a valid percentage");
    println!("25% of 80 is {}", discount.of(80));
    match Percent::new(150) {
        Ok(_) => println!("accepted"),
        Err(e) => println!("rejected: {e:?}"),
    }
}
25% of 80 is 20
rejected: TooLarge(150)

Percent::new returns a Result, because the number might come from a user. But of can't fail: a Percent can only be built through new, so its value is always 0 to 100. And the expect on Percent::new(25) is fine: 25 is a constant, so failure would be a bug, and the message says what was assumed. (To make sure nobody builds a Percent without new, the field would be made private; module 28 shows how.)

Common mistakes

Your task

apply_discount is part of a pricing library, but it panics on an unknown discount code, which can come straight from a customer's input. Change it to return Result<u32, DiscountError>: an unknown code (codes are case-sensitive) returns Err(DiscountError::UnknownCode(<the code>)), and a known code returns the discounted price in Ok.

Challenge 2

nth_score panics when n is out of range, because indexing panics. The index comes from a user's request, so return a Result instead: use scores.get(n) and turn None into Err(ScoreError::OutOfRange { index: n, len: scores.len() }). Don't use [n] indexing.

Challenge 3

Write a validated type from scratch: a network port that's always usable.

Previous: Source chains: errors with causes Next: A first look at thiserror and anyhow