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"),
}
}
There are three bits of syntax here you may not have seen yet: &'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. We'll cover lifetimes in more detail 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. I prefer the guard here because it keeps the
condition next to the pattern.
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.
You can produce a Result with an if that checks the failure case and an
else branch that returns Ok(...).
The error type is &'static str, so you can return a fixed error message
without defining a new type.
Useful from the Standard Library
- The
ResultconstructorsOk(value)andErr(message)are in the prelude, so you can use them without importing anything.divisor == 0.0detects both positive and negative zero, the failure cases for this exercise.
/// 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_eq!(safe_divide(10.0, 0.0), Err("cannot divide by zero"));
assert_eq!(safe_divide(10.0, -0.0), Err("cannot divide by zero"));
}
This simulates reading a file: return an error for an empty filename and the
fixed content for any other name. It uses the same pattern as safe_divide, but
returns 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_emptychecks whether the filename is empty.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_eq!(read_config_file(""), Err("File not found"));
}
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. The memory and ownership chapter explains why a
returned reference must not outlive the value it borrows.
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. You don't need to allocate aStringwithto_string().
/// 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:
"not a valid percentage" when the text cannot be parsed as a u8,
including negatives and values above 255."percentage must be between 0 and 100" when parsing succeeds but the
value is above 100.So "255%" is out of range, while "256" is a parse error for this exercise.
Only one trailing % is allowed.
The error type here is &'static str, so use string literals for the messages.
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::parsetries to parse the text and returns aResult. The turbofish (parse::<u8>()) tells it which numeric type to produce.u8already rejects negative numbers and anything above255, so those inputs produce parse errors.Result::map_errtransforms the error value without touchingOk. You can use it 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. If the text cannot be parsed as u8, returns
/// `Err("not a valid percentage")`. A parsed value above 100 returns
/// `Err("percentage must be between 0 and 100")`. Only one trailing `%` is
/// allowed.
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_eq!(
parse_percentage("101"),
Err("percentage must be between 0 and 100")
);
assert_eq!(
parse_percentage("255%"),
Err("percentage must be between 0 and 100")
);
// These cannot be parsed as u8, unlike 101 and 255 above.
assert_eq!(parse_percentage("256"), Err("not a valid percentage"));
assert_eq!(parse_percentage("-1"), Err("not a valid percentage"));
assert_eq!(parse_percentage("half"), Err("not a valid percentage"));
assert_eq!(parse_percentage(""), Err("not a valid percentage"));
assert_eq!(parse_percentage("75%%"), Err("not a valid percentage"));
}
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.
Extra practice to explore at your own pace. These chapters do not count toward course progress.