Error handling
Option already means a value might be absent. Result means an operation might fail and still return a reason. Learn the enum and the ? operator first, then how a library names those reasons, then where a panic is still the right tool.
Result and ?
Result is an enum with two variants: Ok(T) and Err(E). Failure is a value, the same way None is a value.
The function returns. It does not crash. The caller decides which arm to take.
? can change the error type when From is implemented. You do not need to write that trait yet. You need to see that the error leaving parse_port can become the error leaving bind_addr. opt.ok_or(err)? turns absence into an error. res.ok() drops the error and leaves an Option. let _ = fallible() and fallible().ok() throw the failure away. Handle it, use ?, or expect at the edge of a binary. main itself may return Result<(), Box<dyn std::error::Error>>. The box lets different errors share one return type. Trait objects are explained with traits. The ? in main is the idea here.
fn parse_port(raw: &str) -> Result<u16, String> {
let n: u16 = raw.parse().map_err(|_| String::from("port"))?;
if n == 0 {
return Err(String::from("port must be non-zero"));
}
Ok(n)
}
fn bind_addr(host: &str, port: &str) -> Result<String, String> {
let port = parse_port(port)?;
Ok(format!("{host}:{port}"))
}
fn main() {
println!("{}", bind_addr("127.0.0.1", "8080").unwrap());
}Designing error types
Production code uses two layers.
| Layer | Error shape |
|---|---|
| Library or domain crate | A typed enum so callers can match variants. Real projects often derive it with thiserror. These lessons stay on the standard library, so the enum is written by hand. |
| Application binary | Wraps errors with context, often anyhow::Result or Box of dyn Error plus Send plus Sync. The report matters more than matching every dependency. |
MissingKey and InvalidPort are variants a caller can match. Display and the Error trait make the type printable and chainable.
| Kind of failure | Tool |
|---|---|
| I/O, user input, network, parsing | Result. The caller can act. |
| A broken invariant, a programmer bug | Panic. It should be impossible if the types held. |
The standard Error trait requires Debug and Display, and source returns another error or nothing.
#[derive(Debug)]
enum ConfigError {
MissingKey(&'static str),
InvalidPort(String),
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::MissingKey(k) => write!(f, "missing {k}"),
Self::InvalidPort(v) => write!(f, "bad port {v}"),
}
}
}
impl std::error::Error for ConfigError {}
fn load_port(raw: Option<&str>) -> Result<u16, ConfigError> {
let raw = raw.ok_or(ConfigError::MissingKey("PORT"))?;
raw.parse().map_err(|_| ConfigError::InvalidPort(raw.to_string()))
}
fn main() {
println!("{:?}", load_port(Some("8080")));
}Panic and unwrap
| Where | Policy |
|---|---|
| A library’s public API | Return Result or Option. unwrap and expect appear only on an invariant proved in that function. |
| A binary | Use ? up to main. expect is acceptable when the environment is wrong and the process must stop. Then log and set an exit code. |
| Tests | unwrap is fine. A loud test failure is the point. |
catch_unwind is rare: an FFI boundary, or isolating a crash. It is not how you handle a normal error. Prefer an enum or a newtype so an assert is unnecessary.
| Method | On failure |
|---|---|
| unwrap | Panics, with no message |
| expect("why") | Panics, with your message |
unwrap_or | Uses the default you pass |
unwrap_or_else | Computes the default only when needed |
unwrap_or_default | Uses the type’s default |
assert! | Fails the process when the condition is wrong |
debug_assert! | Same check, removed in a release build. Use it for expensive checks. |
Return Result or Option. That includes validation of user input.
fn main() {
let key = std::env::var("APP_KEY").expect("APP_KEY is required");
let port: u16 = std::env::var("PORT")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(8080);
println!("key_len={} port={port}", key.len());
}