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
- Panicking in a library on bad input. A web server using your parsing library would crash on one bad request. Return a
Result. - Returning
Resultfor bugs. If the only way to get anErris a programming mistake, callers end up writing.unwrap()everywhere anyway. Panic (with a clear message) and document it. unwrapwith no explanation. When failure would be a bug,expect("why this can't fail")documents the invariant for the next reader.
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.
PortError(deriveDebugandPartialEq) with variantsNotANumber(ParseIntError),ZeroandReserved(u16)struct Port(u16);impl Portwithfn parse(text: &str) -> Result<Port, PortError>: trim and parse asu16(a parse error becomesNotANumber), then reject0(Zero) and anything below 1024 (Reserved(port)), andfn number(&self) -> u16, which returns the port number
Previous: Source chains: errors with causes Next: A first look at thiserror and anyhow