How it works
The explanation, with examples you can run.
Your own error type, and ?
use std::fmt;use std::num::ParseIntError; #[derive(Debug)]enum ConfigError { Missing(&'static str), BadNumber(ParseIntError),} impl fmt::Display for ConfigError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { ConfigError::Missing(k) => write!(f, "missing key {k}"), ConfigError::BadNumber(e) => write!(f, "bad number: {e}"), } }} impl std::error::Error for ConfigError {} impl From<ParseIntError> for ConfigError { // lets ? convert the error for us fn from(e: ParseIntError) -> Self { ConfigError::BadNumber(e) }} fn workers(raw: Option<&str>) -> Result<u32, ConfigError> { let text = raw.ok_or(ConfigError::Missing("workers"))?; let n: u32 = text.trim().parse()?; // ParseIntError -> ConfigError via From Ok(n)} fn main() -> Result<(), Box<dyn std::error::Error>> { println!("{:?}", workers(Some(" 8 "))); println!("{}", workers(None).unwrap_err()); println!("{}", workers(Some("eight")).unwrap_err()); let n = workers(Some("4"))?; // in main too, because main returns Result println!("running {n} workers"); Ok(())}Output: Ok(8), missing key workers, bad number: invalid digit found in string, running 4 workers.
expr? means: if expr is Ok(v), the value is v; if it is Err(e), return Err(From::from(e)) from the enclosing function right now. The From impl is what lets one function call code with different error types and still return its own. ? also works on Option in a function returning Option. ok_or converts an Option into a Result.
main can return Result<(), E> for any E: Debug. Returning Err prints the error and exits with a non-zero status. Box<dyn std::error::Error> accepts any error type, through a blanket From impl.
Choosing error types
- Libraries usually return an enum like
ConfigError, so callers canmatchon the cases. Thethiserrorcrate derives theDisplay,ErrorandFromboilerplate above. - Applications often care more about a good message than about matching.
Box<dyn Error>or theanyhowcrate collect any error and let you add context (.context("reading config")). - Implement
Error::source()(or letthiserrordo it) so a chain of causes can be printed, much like Go's wrapped errors.
Panics
panic!, unwrap(), expect(), array indexing out of bounds and integer overflow in debug builds all panic. By default a panic unwinds the current thread's stack, running Drop for every value as it goes, and ends the thread; in the main thread that ends the program. Setting panic = "abort" in a Cargo profile skips unwinding and makes binaries smaller. std::panic::catch_unwind exists for FFI boundaries and thread pools, not for ordinary error handling.
A useful habit: convert errors at boundaries. Inside a module, use a precise error type; at the edge of the program (an HTTP handler, main), turn it into a status code or a message for a human, and log the full chain of source() causes once.
Use expect("why this can't fail") rather than bare unwrap() when you do assert something: the message is the documentation.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.