Errors#
Rust has no exceptions. Recoverable failure is the
Result<T, E> enum; absence is Option<T>; unrecoverable
bugs use panic!. The ? operator propagates Err /
None up the stack with the same readability as exceptions.
thiserror and anyhow are the two crates the operator
reaches for to make error types either precise or convenient.
/operators`.
Result and Option#
enum Result<T, E> { Ok(T), Err(E) }
enum Option<T> { Some(T), None }
fn parse_port(s: &str) -> Result<u16, std::num::ParseIntError> {
s.parse::<u16>()
}
fn first<T>(xs: &[T]) -> Option<&T> { xs.first() }
Use Result for “something might fail with a
diagnosable reason” and Option for “something might be
absent”.
The ? operator#
? unwraps Ok/Some and early-returns
Err/None. The function’s return type must be
Result/Option accordingly.
fn load_config(path: &str) -> std::io::Result<Config> {
let text = std::fs::read_to_string(path)?; // io::Error
let cfg: Config = toml::from_str(&text).map_err(|e|
std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
Ok(cfg)
}
? calls From::from on the error, so different Err
types coexist as long as conversions exist.
Custom errors (thiserror)#
For library code: define a typed error enum, derive
thiserror::Error.
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("io: {0}")]
Io(#[from] std::io::Error),
#[error("invalid TOML: {0}")]
Toml(#[from] toml::de::Error),
#[error("missing field: {0}")]
MissingField(&'static str),
}
fn load(path: &str) -> Result<Config, ConfigError> {
let text = std::fs::read_to_string(path)?; // Io
let cfg: Config = toml::from_str(&text)?; // Toml
Ok(cfg)
}
The #[from] annotation auto-generates impl
From<std::io::Error>.
anyhow for applications#
For binary code where do not care about
catching specific error kinds, anyhow::Error is a type-erased
wrapper that prints a chain of contexts.
use anyhow::{Context, Result};
fn run() -> Result<()> {
let path = std::env::var("CONFIG").context("CONFIG env var")?;
let cfg = load(&path).with_context(|| format!("loading {path}"))?;
process(&cfg).context("processing")?;
Ok(())
}
fn main() -> Result<()> { run() }
context and with_context chain extra information; the
final error prints the whole chain.
unwrap and expect#
unwrap() and expect("msg") panic when the value is
Err / None. Used in tests, in main for tutorials,
and in code paths where an error genuinely cannot happen.
let port: u16 = "8080".parse().expect("hardcoded port should parse");
For production code prefer explicit handling.
unwrap_or, unwrap_or_else, unwrap_or_default, and
ok_or, ok_or_else, and_then, map cover most
patterns.
let host = opts.host.unwrap_or_else(|| "localhost".to_string());
let port = opts.port.unwrap_or(8080);
let n: Result<u32, _> = "42".parse();
let v = n.unwrap_or(0);
panic!#
panic!(msg) aborts the current thread (by default unwinds
the stack, runs Drop impls, propagates to the join handle).
Use for programmer errors: violated invariants, impossible
states, bugs you want the test suite to surface.
if !buffer.is_aligned() {
panic!("buffer must be 4-byte aligned");
}
unreachable!() is the operator’s marker for code paths the
type system cannot prove unreachable.
match version {
1 => v1_handler(),
2 => v2_handler(),
_ => unreachable!("version validated upstream"),
}
debug_assert! only fires in debug builds; useful for hot
paths.
debug_assert!(invariant_holds(&state));
Cargo.toml panic mode#
In Cargo.toml, pick abort (smaller
binary, faster, no cleanup) or unwind (default, runs Drop,
catchable with catch_unwind).
[profile.release]
panic = "abort"
Conversions across error types#
When ? needs to bridge between error types, implement
From.
impl From<std::io::Error> for AppError {
fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}
thiserror’s #[from] does this automatically; the
operator only writes it by hand for tricky conversions.
Returning errors from main#
main can return Result<(), E> where E: Debug; the
runtime prints the error with Debug and exits non-zero.
fn main() -> anyhow::Result<()> {
run().context("startup failed")?;
Ok(())
}