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"),
}
}
You only need a working reading of three details in this example: &'static str, the ::<u16> after parse, and the if guard on the first match arm.
&'static strThis 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 receive more detailed treatment later.
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.
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",
}
As with Option, you can handle a Result with match, if let, or a combinator when you have a simple fallback:
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.
The ? operator chains fallible operations without repeating the same match boilerplate each time.
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
ResultconstructorsOk(value)andErr(message)are in the prelude, so you can use them without importing anything.f64 == 0.0detects the failure case. Floating-point comparison has plenty of nasty edge cases in general, but checking for exact zero is fine here.Result::is_erris what the test uses; you don't need it inside the function.
/// Divides `dividend` by `divisor`.
///
/// Returns `Ok(quotient)` when the division is well-defined, or
/// `Err("cannot divide by zero")` when `divisor` is `0.0`.
///
/// Start here. The simplest way to produce a `Result`: an `if` checks
/// the failure case, 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.
fn safe_divide(dividend: f64, divisor: f64) -> Result<f64, &'static str> {
if divisor == 0.0 {
Err("cannot divide by zero")
} else {
Ok(dividend / divisor)
}
}
#[test]
fn test_safe_divide() {
assert_eq!(safe_divide(10.0, 2.0), Ok(5.0));
assert_eq!(safe_divide(-9.0, 3.0), Ok(-3.0));
assert!(safe_divide(10.0, 0.0).is_err());
}
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_emptyis the cleanest way to detect an empty filename.String::fromor.to_string()turns the literal"config content"into the ownedStringtheOkarm needs.
/// Reads a configuration file (simulated).
/// Returns Ok(content) normally, Err("File not found") for empty input.
///
/// 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.
fn read_config_file(filename: &str) -> Result<String, &'static str> {
if filename.is_empty() {
Err("File not found")
} else {
Ok(String::from("config content"))
}
}
#[test]
fn test_read_config_file() {
assert_eq!(
read_config_file("app.toml"),
Ok("config content".to_string())
);
assert!(read_config_file("").is_err());
}
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.
That lifetime relationship becomes explicit in the memory and ownership material.
For now, notice that the function compiles even though no lifetime appears in the signature.
Useful from the standard library
str::containstakes achar(or another&str) and answers yes/no. Soemail.contains('@')is exactly the check you need.- The
Okbranch can return the input slice directly: it's already a&strwith the right lifetime. Noto_string()allocation needed.
/// Validates an email address (basic check).
/// Returns Ok(email) if contains '@', Err(message) otherwise.
///
/// 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.
fn validate_email(email: &str) -> Result<&str, &'static str> {
if email.contains('@') {
Ok(email)
} else {
Err("email must contain '@'")
}
}
#[test]
fn test_validate_email() {
assert_eq!(validate_email("user@example.com"), Ok("user@example.com"));
assert!(validate_email("invalid-email").is_err());
}
A trailing % is allowed, but the remaining text may still fail to parse, and a parsed u8 may be greater than 100.
Those two failures need different error messages.
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_suffixremoves a trailing pattern if present and returnsOption<&str>.input.strip_suffix('%').unwrap_or(input)peels the%when there is one.str::parseis the canonical "may fail to parse" call. The turbofish (parse::<u8>()) tells it which numeric type to produce.u8already rejects negative numbers and anything above255, which catches a couple of cases for free.Result::map_errswaps the error type without touchingOk. Handy to turn the parser's error into your own static message.- A bounds check
if n > 100 { return Err("...") }finishes the job; theu8type already takes care ofn < 0.
/// Parses a percentage from a string. Accepts integers in `0..=100`,
/// optionally with a trailing `%` (so `"42"` and `"42%"` both work).
///
/// Returns `Ok(value)` on success, `Err(message)` otherwise.
///
/// 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.
fn parse_percentage(input: &str) -> Result<u8, &'static str> {
let digits = input.strip_suffix('%').unwrap_or(input);
match digits.parse::<u8>() {
Ok(value) if value <= 100 => Ok(value),
Ok(_) => Err("percentage must be between 0 and 100"),
Err(_) => Err("not a valid percentage"),
}
}
#[test]
fn test_parse_percentage() {
assert_eq!(parse_percentage("0"), Ok(0));
assert_eq!(parse_percentage("42"), Ok(42));
assert_eq!(parse_percentage("100"), Ok(100));
assert_eq!(parse_percentage("75%"), Ok(75));
assert!(parse_percentage("101").is_err());
assert!(parse_percentage("-1").is_err());
assert!(parse_percentage("half").is_err());
assert!(parse_percentage("").is_err());
}
The examples use simple if checks to build Results with owned and borrowed success values.
Combining strip_suffix, parse, and a bounds check produces a validating parser with several failure cases.
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)andErr(error)are the constructors. Both are in the prelude.Resulthas the same combinator family asOption:unwrap_or,map,map_or, plusmap_errfor transforming the error side andokto drop the error and convert toOption<T>.&'static stris a convenient error type when every message is a fixed string literal. Applications often use error enums or ownedStrings once errors need data of their own.- 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 chains fallible calls without requiring amatcheach time. For now,matchkeeps both paths visible.