BBuzzard

A type is an error if it implements std::error::Error, which requires Debug and Display. In a library, an enum of error cases is easiest for callers to match on. Implementing From<Inner> for your error lets ? convert inner errors automatically. source() links an error to the one that caused it, so you can print the whole chain.

In a small binary, Box<dyn Error> is a fine catch-all: any error converts into it with ?.

panic! is for bugs: a broken invariant, an impossible state. By default it unwinds the thread's stack, running destructors. catch_unwind can stop a panic, but it is not a replacement for Result.

The smallest custom error

#[derive(Debug)]
struct EmptyName;

impl fmt::Display for EmptyName {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "empty name")
    }
}

impl Error for EmptyName {}

fn greet(name: &str) -> Result<String, EmptyName> {
    if name.is_empty() {
        return Err(EmptyName);
    }

    Ok(format!("hello {name}"))
}

fn demo_simple_error() {
    println!("-- simple error --");

    println!("{:?} {:?}", greet("Ada"), greet(""));
    if let Err(e) = greet("") {
        println!("display: {e}");
    }
}

An error enum, matched by the caller

#[derive(Debug)]
enum ConfigError {
    Missing(&'static str),
    BadNumber { key: &'static str, source: ParseIntError },
    OutOfRange(u32),
}

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 { key, .. } => write!(f, "key {key} is not a number"),
            ConfigError::OutOfRange(n) => write!(f, "{n} is out of range"),
        }
    }
}

impl Error for ConfigError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            ConfigError::BadNumber { source, .. } => Some(source),
            _ => None,
        }
    }
}

fn read_port(value: Option<&str>) -> Result<u32, ConfigError> {
    let text = value.ok_or(ConfigError::Missing("port"))?;
    let n: u32 =
        text.parse().map_err(|e| ConfigError::BadNumber { key: "port", source: e })?;
    if n > 65535 {
        return Err(ConfigError::OutOfRange(n));
    }

    Ok(n)
}

fn demo_error_enum() {
    println!("-- error enum --");

    for input in [Some("8080"), None, Some("x"), Some("99999")] {
        match read_port(input) {
            Ok(p) => println!("port {p}"),
            Err(ConfigError::Missing(k)) => println!("using default for {k}"),
            Err(e) => println!("error: {e}"),
        }
    }
}

Walking the source() chain

fn print_chain(e: &dyn Error) {
    print!("{e}");
    let mut cur = e.source();
    while let Some(inner) = cur {
        print!(" <- caused by: {inner}");
        cur = inner.source();
    }

    println!();
}

fn demo_chain() {
    println!("-- source chain --");

    if let Err(e) = read_port(Some("12a")) {
        print_chain(&e);
    }
}

From lets ? convert between error types

#[derive(Debug)]
enum AppError {
    Parse(ParseIntError),
    Config(ConfigError),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::Parse(_) => write!(f, "parse failed"),
            AppError::Config(_) => write!(f, "config failed"),
        }
    }
}

impl Error for AppError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            AppError::Parse(e) => Some(e),
            AppError::Config(e) => Some(e),
        }
    }
}

impl From<ParseIntError> for AppError {
    fn from(e: ParseIntError) -> Self {
        AppError::Parse(e)
    }
}

impl From<ConfigError> for AppError {
    fn from(e: ConfigError) -> Self {
        AppError::Config(e)
    }
}

fn run(workers: &str, port: Option<&str>) -> Result<(u32, u32), AppError> {
    let w: u32 = workers.parse()?; // ParseIntError -> AppError via From
    let p = read_port(port)?; // ConfigError -> AppError via From
    Ok((w, p))
}

fn demo_from_conversion() {
    println!("-- From and ? --");

    println!("{:?}", run("4", Some("80")).map_err(|e| e.to_string()));
    for (w, p) in [("x", Some("80")), ("4", Some("bad"))] {
        if let Err(e) = run(w, p) {
            print_chain(&e);
        }
    }
}

Box<dyn Error> as a catch-all

fn load_all(a: &str, b: &str) -> Result<i32, Box<dyn Error>> {
    let x: i32 = a.parse()?;
    if x < 0 {
        return Err("negative input".into()); // &str converts into Box<dyn Error>
    }

    let y = greet(b)?.len() as i32;
    Ok(x + y)
}

fn demo_box_dyn() {
    println!("-- Box<dyn Error> --");

    println!("{:?}", load_all("1", "ada").map_err(|e| e.to_string()));
    for (a, b) in [("x", "ada"), ("-1", "ada"), ("1", "")] {
        match load_all(a, b) {
            Ok(v) => println!("ok {v}"),
            Err(e) => println!("error: {e}"),
        }
    }

    // Downcast to recover the concrete type.
    if let Err(e) = load_all("1", "")
        && e.downcast_ref::<EmptyName>().is_some()
    {
        println!("it was EmptyName");
    }
}

Panics, unwinding, and when to use them

fn demo_panic() {
    println!("-- panic --");

    // Silence the default "thread panicked" message for this demo.
    let default_hook = std::panic::take_hook();
    std::panic::set_hook(Box::new(|_| {}));

    let result = std::panic::catch_unwind(|| {
        panic!("boom");
    });

    println!("caught panic! {}", result.is_err());

    let v = vec![1, 2, 3];
    let oob = std::panic::catch_unwind(|| v[10]);
    println!("index out of bounds panics: {}", oob.is_err());

    let msg = std::panic::catch_unwind(|| {
        let n: Option<i32> = None;
        n.expect("must be set")
    });

    if let Err(payload) = msg
        && let Some(s) = payload.downcast_ref::<String>()
    {
        // panic!("literal") carries a &str; a formatted message carries a String.
        println!("payload: {s}");
    }

    std::panic::set_hook(default_hook);

    // unreachable!, todo!, and unimplemented! are panics with fixed messages (lesson 54).
}
$ cargo run --bin 19_errors_panic
-- simple error --
Ok("hello Ada") Err(EmptyName)
display: empty name
-- error enum --
port 8080
using default for port
error: key port is not a number
error: 99999 is out of range
-- source chain --
key port is not a number <- caused by: invalid digit found in string
-- From and ? --
Ok((4, 80))
parse failed <- caused by: invalid digit found in string
config failed <- caused by: key port is not a number <- caused by: invalid digit found in string
-- Box<dyn Error> --
Ok(10)
error: invalid digit found in string
error: negative input
error: empty name
it was EmptyName
-- panic --
caught panic! true
index out of bounds panics: true
payload: must be set

Full program

19_errors_panic/main.rs · 246 lines
View on GitHub
// 19. Custom errors, error chains, and panic
//
// A type is an error if it implements std::error::Error, which requires Debug and
// Display. In a library, an enum of error cases is easiest for callers to match on.
// Implementing From<Inner> for your error lets ? convert inner errors automatically.
// source() links an error to the one that caused it, so you can print the whole chain.
//
// In a small binary, Box<dyn Error> is a fine catch-all: any error converts into it
// with ?.
//
// panic! is for bugs: a broken invariant, an impossible state. By default it unwinds
// the thread's stack, running destructors. catch_unwind can stop a panic, but it is not
// a replacement for Result.
//
// Run: cargo run --bin 19_errors_panic

use std::error::Error;
use std::fmt;
use std::num::ParseIntError;

#[derive(Debug)]
struct EmptyName;

impl fmt::Display for EmptyName {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "empty name")
    }
}

impl Error for EmptyName {}

fn greet(name: &str) -> Result<String, EmptyName> {
    if name.is_empty() {
        return Err(EmptyName);
    }

    Ok(format!("hello {name}"))
}

// Example 1: the smallest custom error.
fn demo_simple_error() {
    println!("-- simple error --");

    println!("{:?} {:?}", greet("Ada"), greet(""));
    if let Err(e) = greet("") {
        println!("display: {e}");
    }
}

#[derive(Debug)]
enum ConfigError {
    Missing(&'static str),
    BadNumber { key: &'static str, source: ParseIntError },
    OutOfRange(u32),
}

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 { key, .. } => write!(f, "key {key} is not a number"),
            ConfigError::OutOfRange(n) => write!(f, "{n} is out of range"),
        }
    }
}

impl Error for ConfigError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            ConfigError::BadNumber { source, .. } => Some(source),
            _ => None,
        }
    }
}

fn read_port(value: Option<&str>) -> Result<u32, ConfigError> {
    let text = value.ok_or(ConfigError::Missing("port"))?;
    let n: u32 =
        text.parse().map_err(|e| ConfigError::BadNumber { key: "port", source: e })?;
    if n > 65535 {
        return Err(ConfigError::OutOfRange(n));
    }

    Ok(n)
}

// Example 2: an error enum, matched by the caller.
fn demo_error_enum() {
    println!("-- error enum --");

    for input in [Some("8080"), None, Some("x"), Some("99999")] {
        match read_port(input) {
            Ok(p) => println!("port {p}"),
            Err(ConfigError::Missing(k)) => println!("using default for {k}"),
            Err(e) => println!("error: {e}"),
        }
    }
}

fn print_chain(e: &dyn Error) {
    print!("{e}");
    let mut cur = e.source();
    while let Some(inner) = cur {
        print!(" <- caused by: {inner}");
        cur = inner.source();
    }

    println!();
}

// Example 3: walking the source() chain.
fn demo_chain() {
    println!("-- source chain --");

    if let Err(e) = read_port(Some("12a")) {
        print_chain(&e);
    }
}

#[derive(Debug)]
enum AppError {
    Parse(ParseIntError),
    Config(ConfigError),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::Parse(_) => write!(f, "parse failed"),
            AppError::Config(_) => write!(f, "config failed"),
        }
    }
}

impl Error for AppError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            AppError::Parse(e) => Some(e),
            AppError::Config(e) => Some(e),
        }
    }
}

impl From<ParseIntError> for AppError {
    fn from(e: ParseIntError) -> Self {
        AppError::Parse(e)
    }
}

impl From<ConfigError> for AppError {
    fn from(e: ConfigError) -> Self {
        AppError::Config(e)
    }
}

fn run(workers: &str, port: Option<&str>) -> Result<(u32, u32), AppError> {
    let w: u32 = workers.parse()?; // ParseIntError -> AppError via From
    let p = read_port(port)?; // ConfigError -> AppError via From
    Ok((w, p))
}

// Example 4: From lets ? convert between error types.
fn demo_from_conversion() {
    println!("-- From and ? --");

    println!("{:?}", run("4", Some("80")).map_err(|e| e.to_string()));
    for (w, p) in [("x", Some("80")), ("4", Some("bad"))] {
        if let Err(e) = run(w, p) {
            print_chain(&e);
        }
    }
}

fn load_all(a: &str, b: &str) -> Result<i32, Box<dyn Error>> {
    let x: i32 = a.parse()?;
    if x < 0 {
        return Err("negative input".into()); // &str converts into Box<dyn Error>
    }

    let y = greet(b)?.len() as i32;
    Ok(x + y)
}

// Example 5: Box<dyn Error> as a catch-all.
fn demo_box_dyn() {
    println!("-- Box<dyn Error> --");

    println!("{:?}", load_all("1", "ada").map_err(|e| e.to_string()));
    for (a, b) in [("x", "ada"), ("-1", "ada"), ("1", "")] {
        match load_all(a, b) {
            Ok(v) => println!("ok {v}"),
            Err(e) => println!("error: {e}"),
        }
    }

    // Downcast to recover the concrete type.
    if let Err(e) = load_all("1", "")
        && e.downcast_ref::<EmptyName>().is_some()
    {
        println!("it was EmptyName");
    }
}

// Example 6: panics, unwinding, and when to use them.
fn demo_panic() {
    println!("-- panic --");

    // Silence the default "thread panicked" message for this demo.
    let default_hook = std::panic::take_hook();
    std::panic::set_hook(Box::new(|_| {}));

    let result = std::panic::catch_unwind(|| {
        panic!("boom");
    });

    println!("caught panic! {}", result.is_err());

    let v = vec![1, 2, 3];
    let oob = std::panic::catch_unwind(|| v[10]);
    println!("index out of bounds panics: {}", oob.is_err());

    let msg = std::panic::catch_unwind(|| {
        let n: Option<i32> = None;
        n.expect("must be set")
    });

    if let Err(payload) = msg
        && let Some(s) = payload.downcast_ref::<String>()
    {
        // panic!("literal") carries a &str; a formatted message carries a String.
        println!("payload: {s}");
    }

    std::panic::set_hook(default_hook);

    // unreachable!, todo!, and unimplemented! are panics with fixed messages (lesson 54).
}

fn main() {
    demo_simple_error();
    demo_error_enum();
    demo_chain();
    demo_from_conversion();
    demo_box_dyn();
    demo_panic();
}