← Back to Rust series
🦀
Error handling
Error handling · Prerequisite: lesson 17

18. Custom Error Types (thiserror, From)

Production libraries and services define concrete error enums so callers can branch on category — rather than rely on Box<dyn Error>. This lesson defines an error enum from scratch, implements Display, the Error trait, and From by hand, then collapses all of it with the thiserror crate.

Rustthiserrorerror typeFromError trait
Duration
~1.5 hours
Level
📊 Intermediate
Prerequisite
🎯 Lesson 17
OUTCOME
Production libraries and services define concrete error enums so callers can branch on category — rather than rely on Box<dyn Error>. This lesson defines an error enum from scratch, implements Display, the Error trait, and From by hand, then collapses all of it with the thiserror crate.

What you'll learn

  • 1Define an enum of domain-specific error variants
  • 2Implement Display and Error by hand
  • 3Wire From<T> for automatic conversion
  • 4Use thiserror to cut the boilerplate to one line
  • 5Tell apart anyhow and thiserror use cases

Overview

A library must tell its callers what **kind** of failure happened — `MyError::NotFound`, `MyError::PermissionDenied`, etc. — so the caller can branch. This lesson does it by hand once, then shows how thiserror reduces it to a one-liner per variant.

Core Concepts

1) Hand-rolled custom error

rust
use std::fmt;

#[derive(Debug)]
enum AppError {
    NotFound(String),
    Invalid(String),
    Io(std::io::Error),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::NotFound(s) => write!(f, "not found: {}", s),
            AppError::Invalid(s)  => write!(f, "invalid: {}", s),
            AppError::Io(e)       => write!(f, "io: {}", e),
        }
    }
}

impl std::error::Error for AppError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            AppError::Io(e) => Some(e),
            _ => None,
        }
    }
}

impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}

2) thiserror — one-line equivalents

toml
[dependencies]
thiserror = "1"
rust
use thiserror::Error;

#[derive(Debug, Error)]
enum AppError {
    #[error("not found: {0}")]
    NotFound(String),
    #[error("invalid: {0}")]
    Invalid(String),
    #[error("io: {0}")]
    Io(#[from] std::io::Error),  // auto-derive From
}

3) thiserror vs. anyhow

CrateRoleUse case
thiserrorLibrary-grade concrete error enums via macroReusable libraries
anyhowApp-grade Box<dyn Error> wrapper + contextBinaries / services

Hands-on Examples

thiserror plus `?` auto-conversion:

rust
use thiserror::Error;
use std::fs;
use std::num::ParseIntError;

#[derive(Debug, Error)]
enum AppError {
    #[error("file: {0}")]
    Io(#[from] std::io::Error),
    #[error("parse: {0}")]
    Parse(#[from] ParseIntError),
    #[error("empty file")]
    Empty,
}

fn first_number(path: &str) -> Result<i32, AppError> {
    let s = fs::read_to_string(path)?;           // io::Error → AppError
    let line = s.lines().next().ok_or(AppError::Empty)?;
    Ok(line.trim().parse::<i32>()?)              // ParseIntError → AppError
}

fn main() -> Result<(), AppError> {
    println!("{}", first_number("data.txt")?);
    Ok(())
}

Branch on variants:

rust
match first_number("data.txt") {
    Ok(n)                       => println!("ok: {}", n),
    Err(AppError::Empty)        => eprintln!("file was empty"),
    Err(AppError::Io(e))        => eprintln!("I/O failed: {}", e),
    Err(AppError::Parse(e))     => eprintln!("not a number: {}", e),
}

Common Mistakes

Q. Can I just always use Box<dyn Error>?

A. Fine for prototypes / main. In a library it deprives callers of variant-level branching — they have to downcast every time, which is brittle.

Q. thiserror and anyhow together?

A. Common combo. Library modules define concrete errors with thiserror; the binary that uses them returns `anyhow::Result` and attaches `.context(...)` strings.

Q. Do I really need to implement Error?

A. Required to interop with other libraries — you need it to fit into `Box<dyn Error>` and to follow the `source()` chain.

Recap

  • Enum variants encode error categories so callers can branch
  • Display / Error / From by hand, or thiserror in one line
  • Libraries = thiserror, applications = anyhow (often mixed)
  • `?` relies on the From trait for auto-conversion

Try It Yourself

  1. Hand-write a custom error for file I/O + integer parsing (Display, Error, From)
  2. Rewrite the same with thiserror and compare line counts
  3. Add `#[from]` to a variant and verify `?` auto-converts the source error
Example code / lecture materials

All lecture materials and example code are openly available on GitHub.

View on GitHub ↗