RustMiddleTechnical

Как организовать error handling в приложении через thiserror, anyhow и custom error types?

thiserror автоматически генерирует Display и From для типизированных enum-ошибок библиотек. anyhow даёт ergonomic Box<dyn Error> для приложений с контекстом через .context(). Комбинация: библиотеки используют thiserror, бинарники — anyhow.

Error handling в Rust: thiserror, anyhow и кастомные типы

Кастомные типы без зависимостей

Базовый подход — enum с вариантами ошибок, реализующий std::error::Error, Display и From для конвертации.

use std::fmt;
use std::num::ParseIntError;
use std::io;

#[derive(Debug)]
pub enum ConfigError {
    Io(io::Error),
    Parse(ParseIntError),
    MissingKey(String),
}

impl fmt::Display for ConfigError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ConfigError::Io(e) => write!(f, "IO error: {}", e),
            ConfigError::Parse(e) => write!(f, "parse error: {}", e),
            ConfigError::MissingKey(k) => write!(f, "missing key: {}", k),
        }
    }
}

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

impl From<io::Error> for ConfigError {
    fn from(e: io::Error) -> Self { ConfigError::Io(e) }
}
impl From<ParseIntError> for ConfigError {
    fn from(e: ParseIntError) -> Self { ConfigError::Parse(e) }
}

thiserror: для библиотечных ошибок

Крейт thiserror генерирует весь boilerplate через derive-макросы. Версия 1.x стабильна, 2.x — с улучшенной диагностикой.

// Cargo.toml: thiserror = "2"
use thiserror::Error;

#[derive(Debug, Error)]
pub enum AppError {
    #[error("database error: {0}")]
    Database(#[from] sqlx::Error),

    #[error("not found: {id}")]
    NotFound { id: u64 },

    #[error("validation failed: {field} — {message}")]
    Validation { field: String, message: String },

    #[error("IO error")]
    Io(#[from] std::io::Error),
}

async fn get_user(id: u64, pool: &sqlx::PgPool) -> Result<User, AppError> {
    let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id as i64)
        .fetch_optional(pool)
        .await?;  // sqlx::Error -> AppError::Database через From

    user.ok_or(AppError::NotFound { id })
}

anyhow: для приложений и бинарников

anyhow::Error — динамический тип ошибки с backtrace и контекстом. Идеален для main(), CLI и мест, где тип ошибки не важен вызывающему коду.

// Cargo.toml: anyhow = "1"
use anyhow::{Context, Result, bail, ensure};

fn load_config(path: &str) -> Result<Config> {
    let content = std::fs::read_to_string(path)
        .with_context(|| format!("failed to read config from '{}'", path))?;

    let config: Config = serde_json::from_str(&content)
        .context("invalid JSON in config file")?;

    ensure!(config.port > 1024, "port must be > 1024, got {}", config.port);

    Ok(config)
}

fn main() -> Result<()> {
    let config = load_config("config.json")?;
    println!("Listening on port {}", config.port);
    Ok(())
}

// bail! — ранний возврат с ошибкой
fn check_age(age: u32) -> Result<()> {
    if age < 18 {
        bail!("age must be >= 18, got {}", age);
    }
    Ok(())
}

Стратегия: библиотека + приложение

// В lib.rs: thiserror для типизированных ошибок
#[derive(Debug, thiserror::Error)]
pub enum LibError {
    #[error("resource not found: {0}")]
    NotFound(String),
}

// В main.rs / handlers: anyhow для оркестрации
use anyhow::Result;

fn run() -> Result<()> {
    let result = my_lib::do_something()
        .context("failed during startup")?;  // LibError -> anyhow::Error
    Ok(())
}

Подводные камни

  • Смешивание anyhow::Error в публичном API библиотеки скрывает типы ошибок от пользователей — они не смогут сматчить варианты.
  • #[from] в thiserror создаёт From-имплементацию; два варианта с одним и тем же #[from]-типом вызовут конфликт.
  • Backtrace в anyhow требует RUST_BACKTRACE=1 или RUST_LIB_BACKTRACE=1 — без них трассировки нет даже в debug-сборке.
  • Не применяйте .unwrap() в production-библиотечном коде — паника пересекает crate-границу без контекста.
  • anyhow::Error не реализует std::error::Error напрямую — нельзя передать в API, ожидающий Box<dyn Error>, без .into().
  • Цепочка source() обрывается, если промежуточный тип не реализует Error::source() — проверяйте полноту цепочки при отладке.

Common mistakes

  • Отвечать определением без production-сценария.
  • Не называть runtime boundary, security boundary или failure mode.
  • Игнорировать версию API, observability и тестовую проверку.

What the interviewer is testing

  • Объясняет механизм своими словами и без выдуманных API.
  • Называет реальные риски, диагностику и критерий корректности.
  • Связывает ответ с текущей документацией и миграционными ограничениями.

Sources

Related topics