🎓 A1Learner
🔍
0 of 142 lessons completed

Rust Choosing an Error Type

Choosing an error type means picking how a failure will travel to its caller. What must the caller be able to do with the failure?

One failure, three carriers

The same impossible temperature, carried each way in turn. First, as the typed enum:

let typed: Result<f64, CelsiusError> = check_celsius(-300.0);
println!("typed: {}", typed.unwrap_err());

Typed: the caller can match

One variant per reaction. The caller sees the value that failed, too.

#[derive(Debug, Error)]
enum CelsiusError {
    #[error("temperature {0} is below absolute zero")]
    BelowAbsoluteZero(f64),
}

Reported: the caller only prints

The same error, boxed. The message survives. The type is hidden.

let boxed: Box<dyn std::error::Error> =
    Box::new(CelsiusError::BelowAbsoluteZero(-300.0));
println!("boxed prints the same: {}", boxed);

Kinds: only the category travels

An io-style kind answers "which sort of failure?" and nothing more.

let code = std::io::Error::from(std::io::ErrorKind::NotFound);
println!("kind = {:?}", code.kind());

Layers convert at the border

A library returns its enum. The application converts it into anyhow with the same ?. Then it adds its own notes.

fn run() -> anyhow::Result<f64> {
    Ok(check_celsius(-300.0)?)
}

Example

use thiserror::Error;

#[derive(Debug, Error)]
enum CelsiusError {
    #[error("temperature {0} is below absolute zero")]
    BelowAbsoluteZero(f64),
}

fn check_celsius(t: f64) -> Result<f64, CelsiusError> {
    if t < -273.15 {
        Err(CelsiusError::BelowAbsoluteZero(t))
    } else {
        Ok(t)
    }
}

fn main() {
    println!("typed: {}", check_celsius(-300.0).unwrap_err());
    match check_celsius(-300.0) {
        Err(CelsiusError::BelowAbsoluteZero(t)) => println!("caller can match it: {t} is impossible"),
        _ => {}
    }
    let boxed: Box<dyn std::error::Error> = Box::new(CelsiusError::BelowAbsoluteZero(-300.0));
    println!("boxed prints the same: {}", boxed);
    let code = std::io::Error::from(std::io::ErrorKind::NotFound);
    println!("kind code style: kind = {:?}", code.kind());
}

Key points

  • The caller's needs decide which error type fits a job.
  • The detail is only for human eyes → a message error (anyhow!, or even a String in a quick internal tool) is honest and cheap
  • Typed enums cost upkeep.
  • Every new failure kind is a new public variant.
  • Public variants are a promise.
  • Other code matches on that promise.
  • Keep the Book's rule over all of it.
  • Expected failures travel as Results.
  • Panics are only for broken promises.

In this example, one failure is carried three ways. As the typed enum, the caller can match BelowAbsoluteZero. The caller can also read the -300 back out of it. That is the shape to choose when reactions differ. Boxed, the very same error prints the identical sentence, but the caller can no longer match it: report-only. The last two lines show the third shape. It is an io-style kind. There is no message crafting at all. It is just a category a caller can switch on. Same failure — the right carrier depends on what the caller must do next.

Note

Start simple and leave room.

A typed enum can carry one transparent "anything else" variant.

The enum can then grow new kinds later.

Callers do not break.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

A library's callers must retry only when the failure was "busy", and report all other failures. Which error shape fits?