Why this matters
Programs fail. Files are missing, input is malformed, disks fill up, networks drop. How a language reports failure shapes how reliable its programs are. Exceptions can be thrown from anywhere and forgotten until they crash production; C's error codes are easy to ignore. Rust splits failures into two kinds: unrecoverable bugs, which stop the program with panic!, and recoverable errors, which are ordinary return values of type Result. Because a Result is a value, you cannot use the success value without deciding what to do about the error.
panic!: stop, this is a bug
fn main() {
let v = vec![1, 2, 3];
let idx: usize = "7".parse().unwrap();
println!("about to index");
let x = v[idx];
println!("{x}");
}
about to index
thread 'main' panicked at main.rs:5:14:
index out of bounds: the len is 3 but the index is 7
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
A panic prints a message with the source location, unwinds the stack (dropping every live value, so files are closed and memory freed), and ends the program with exit status 101. You can panic yourself with panic!("message {}", value). Out-of-bounds indexing, integer overflow in debug builds, unwrap on None, and division by zero all panic.
Panic is for situations that mean the program is wrong: a broken invariant, an impossible state. Continuing would risk wrong results or corrupted data, so stopping loudly is the safe choice.
Result: this can fail, and the caller decides
enum Result<T, E> {
Ok(T),
Err(E),
}
Result<T, E> holds either a success value of type T or an error value of type E. You have used it since module 2: parse returns Result<T, ParseIntError>, read_line returns Result<usize, io::Error>.
fn parse_age(s: &str) -> Result<u8, String> {
let n: i64 = match s.trim().parse() {
Ok(n) => n,
Err(e) => return Err(format!("{s:?} is not a number ({e})")),
};
if n < 0 || n > 150 {
return Err(format!("{n} is not a plausible age"));
}
Ok(n as u8)
}
fn main() {
for input in ["42", "-3", "abc", "200"] {
match parse_age(input) {
Ok(age) => println!("age {age}"),
Err(msg) => println!("error: {msg}"),
}
}
}
age 42
error: -3 is not a plausible age
error: "abc" is not a number (invalid digit found in string)
error: 200 is not a plausible age
- The signature
-> Result<u8, String>documents that the function can fail and what you get in each case. Ok(value)andErr(error)construct the two variants (no prefix needed).- The caller must look inside to get the
u8; the error cannot be silently ignored. - Here the error type is
String, a human-readable message. That is fine for small programs; lesson 4 shows proper error types.
If you call a function returning Result and ignore the value entirely, the compiler warns:
fn might_fail() -> Result<(), String> {
Err(String::from("oops"))
}
fn main() {
might_fail();
}
warning: unused `Result` that must be used
--> main.rs:6:5
|
6 | might_fail();
| ^^^^^^^^^^^^
|
= note: this `Result` may be an `Err` variant, which should be handled
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
6 | let _ = might_fail();
| +++++++
Result is marked #[must_use]. If you really mean to ignore it, write let _ = might_fail();.
Handling a Result
The same toolbox as Option, plus error-specific methods:
fn main() {
let good: Result<i32, String> = "12".parse::<i32>().map_err(|e| e.to_string());
let bad: Result<i32, String> = "x".parse::<i32>().map_err(|e| e.to_string());
println!("{:?} {:?}", good, bad);
println!("{}", bad.clone().unwrap_or(0));
println!("{}", bad.clone().unwrap_or_else(|e| { println!(" (fallback because: {e})"); -1 }));
println!("{:?}", good.clone().map(|n| n * 10));
println!("{} {}", good.is_ok(), bad.is_err());
println!("{:?}", bad.clone().ok());
if let Err(e) = &bad {
println!("error was: {e}");
}
}
Ok(12) Err("invalid digit found in string")
0
(fallback because: invalid digit found in string)
-1
Ok(120)
true true
None
error was: invalid digit found in string
(The .clone() calls are only there so the example can keep using good and bad: methods like unwrap_or, map and ok take the Result by value and consume it.)
match,if let Ok(v)/if let Err(e),let Ok(v) = ... else { ... }unwrap()/expect("why"): the value or a panic. Panicking on anErryou could handle turns a recoverable error into a crash; reserve it for "cannot happen" cases and prototypes.unwrap_or(default),unwrap_or_else(|err| ...)maptransforms theOkvalue;map_errtransforms the error (used above to turn aParseIntErrorinto aString).ok()converts toOption, discarding the error;is_ok(),is_err()test.
Exiting with an error status
Command-line programs report failure through their exit status: 0 means success, anything else failure. Shell scripts and other programs check it (if ./prog; then ...). In Rust, std::process::exit(code) ends the program immediately with that status:
use std::process;
fn main() {
let input = "not-a-number";
let n: i32 = match input.parse() {
Ok(n) => n,
Err(e) => {
eprintln!("error: cannot parse {input:?}: {e}");
process::exit(1);
}
};
println!("{n}");
}
error: cannot parse "not-a-number": invalid digit found in string
The error message goes to stderr (so it is not mixed into the program's real output), and the status is 1. Note that process::exit does not run destructors of live values; for simple programs that is fine. Lesson 3 shows a cleaner way, returning a Result from main.
Choosing between panic and Result
| situation | use |
|---|---|
| bad user input, missing file, network error, invalid data | Result, the caller decides |
| a bug: violated invariant, "impossible" state | panic! (or unreachable!, assert!) |
| quick prototype, test code, examples | unwrap/expect is acceptable |
| you know more than the compiler (e.g. parsing a literal) | expect("reason") |
A useful question: could a correct program encounter this? A correct program will meet malformed input; it should handle it. A correct program will never index past the end of its own list; if it does, panic.
assert!(condition, "message") panics when a condition is false. It documents and enforces invariants:
fn average(values: &[f64]) -> f64 {
assert!(!values.is_empty(), "average of an empty slice");
values.iter().sum::<f64>() / values.len() as f64
}
fn main() {
println!("{}", average(&[1.0, 2.0, 6.0]));
println!("{}", average(&[]));
}
3
thread 'main' panicked at main.rs:2:5:
average of an empty slice
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
(Whether an empty slice should be an error the caller handles, Option<f64>, or a bug, assert!, is a design decision. For a library function, Option is friendlier.)
Common mistakes
- Using the
Resultas if it were the value:let n: i32 = "5".parse();is E0308, expectedi32, foundResult<_, _>. Handle it. - Ignoring a
Result: warningunused_must_use. Do something with it. unwrap()on user input: the program crashes with a panic message instead of a helpful error. Match and report.- Printing errors to stdout: use
eprintln!, soprog > out.txtkeeps the output clean. - Exit status 0 on failure: scripts will think everything worked. Exit with a non-zero status.
Recap
panic!stops the program for bugs: message, unwinding, exit status 101.Result<T, E>isOk(T)orErr(E); functions that can fail return it, and callers must handle it.- Tools:
match,if let,unwrap_or(_else),map,map_err,ok,is_ok, and (sparingly)expect. - Report errors on stderr and exit with a non-zero status via
std::process::exit. - Ask "could a correct program hit this?" Yes:
Result. No: panic.
Going further: set RUST_BACKTRACE=1 in the sandbox shell before running a panicking program and read the backtrace: it lists the chain of function calls that led to the panic.