Error Handling

Result, the question mark, typed errors, and the line between a panic and a value you return to the caller.

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.

fallible()returns ResultOk(value)continueErr(e)handle or return
StatusBoth outcomes are values

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.

01_result_and_question_mark.rsRust
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.

LayerError shape
Library or domain crateA 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 binaryWraps errors with context, often anyhow::Result or Box of dyn Error plus Send plus Sync. The report matters more than matching every dependency.
I/O, parsesource errorsConfigErrorlibraryreportbinaryFrom?
StatusThe library names the cases

MissingKey and InvalidPort are variants a caller can match. Display and the Error trait make the type printable and chainable.

Kind of failureTool
I/O, user input, network, parsingResult. The caller can act.
A broken invariant, a programmer bugPanic. It should be impossible if the types held.

The standard Error trait requires Debug and Display, and source returns another error or nothing.

02_designing_error_types.rsRust
#[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

WherePolicy
A library’s public APIReturn Result or Option. unwrap and expect appear only on an invariant proved in that function.
A binaryUse ? up to main. expect is acceptable when the environment is wrong and the process must stop. Then log and set an exit code.
Testsunwrap 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.

MethodOn failure
unwrapPanics, with no message
expect("why")Panics, with your message
unwrap_orUses the default you pass
unwrap_or_elseComputes the default only when needed
unwrap_or_defaultUses 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.
Can the caller act?every failure siteResult / Optionyespanicstate is corrupt
StatusIf the caller can do something useful

Return Result or Option. That includes validation of user input.

03_panic_vs_result.rsRust
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());
}