Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Crate-Level Error Types and Result Aliases
crate 级错误类型与 Result 别名

What you’ll learn: How Java exception habits map to Rust crate-level error enums, how AppError plus AppResult<T> keeps code readable, and how this pattern replaces scattered exception classes in Rust services.
本章将学习: Java 的异常习惯如何迁移到 Rust 的 crate 级错误枚举,AppErrorAppResult<T> 为什么能让代码更整洁,以及这套模式如何替代分散的异常类体系。

Difficulty: 🟡 Intermediate
难度: 🟡 中级

Java developers are used to methods that may throw many different exceptions without showing the full failure contract in the signature.
Java 开发者很习惯“方法可能抛出很多异常,但签名里并不完整展示失败契约”这种做法。

Rust prefers a different style: define one central error enum for the crate and return it explicitly.
Rust 更偏好另一种风格:为整个 crate 定义一个中心错误枚举,然后显式返回它。

The Core Pattern
核心模式

#![allow(unused)]
fn main() {
use thiserror::Error;

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

    #[error("Serialization error: {0}")]
    Json(#[from] serde_json::Error),

    #[error("Validation error: {message}")]
    Validation { message: String },

    #[error("Not found: {entity} with id {id}")]
    NotFound { entity: String, id: String },
}

pub type AppResult<T> = std::result::Result<T, AppError>;
}

The alias is not decorative.
这个别名不是装饰品。

It turns every signature into a house style that stays readable across repository, service, and handler layers.
它会把 repository、service、handler 各层的函数签名统一成一种易读的团队风格。

#![allow(unused)]
fn main() {
pub async fn get_user(id: Uuid) -> AppResult<User> {
    let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
        .fetch_optional(&pool)
        .await?;

    user.ok_or_else(|| AppError::NotFound {
        entity: "user".into(),
        id: id.to_string(),
    })
}
}

Why This Feels Different from Java
这和 Java 的体感为什么不一样

In Java, a method can look simple because the exception contract is partly hidden in runtime behavior.
在 Java 里,一个方法之所以看起来简洁,往往是因为异常契约有一部分被藏进了运行时行为里。

In Rust, failure is part of the function type.
在 Rust 里,失败本身就是函数类型的一部分。

That means callers do not guess; they handle a typed result.
这意味着调用方不是靠猜,而是在处理一个有类型约束的结果。

Replacing Exception Forests
替代一大片异常类森林

Java codebases often end up with many parallel exception classes:
很多 Java 项目最后都会长出一大片平行存在的异常类:

  • ValidationException
    校验异常。
  • UserNotFoundException
    用户不存在异常。
  • RepositoryException
    仓储异常。
  • RemoteServiceException
    远程服务异常。

Rust can pull the same vocabulary into one enum and make the set visible in one place.
Rust 可以把这些失败语义拉回一个枚举里,并且让整个失败集合集中可见。

#![allow(unused)]
fn main() {
#[derive(thiserror::Error, Debug)]
pub enum UserServiceError {
    #[error("validation failed: {0}")]
    Validation(String),

    #[error("user {0} not found")]
    UserNotFound(String),

    #[error("email already exists: {0}")]
    DuplicateEmail(String),

    #[error(transparent)]
    Database(#[from] sqlx::Error),
}
}

Rust’s Answer to @ControllerAdvice
Rust 对 @ControllerAdvice 的回答

Spring Boot usually converts exceptions to HTTP responses in a centralized place.
Spring Boot 往往会在一个集中的位置把异常翻译成 HTTP 响应。

Rust web frameworks usually express that with IntoResponse.
Rust Web 框架通常会用 IntoResponse 来表达同样的职责。

#![allow(unused)]
fn main() {
impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        match self {
            AppError::Validation { message } => (
                StatusCode::BAD_REQUEST,
                Json(ErrorBody {
                    code: "validation_error",
                    message,
                }),
            )
                .into_response(),
            AppError::NotFound { entity, id } => (
                StatusCode::NOT_FOUND,
                Json(ErrorBody {
                    code: "not_found",
                    message: format!("{entity} {id} was not found"),
                }),
            )
                .into_response(),
            AppError::Database(error) => (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ErrorBody {
                    code: "database_error",
                    message: error.to_string(),
                }),
            )
                .into_response(),
            AppError::Json(error) => (
                StatusCode::BAD_REQUEST,
                Json(ErrorBody {
                    code: "invalid_json",
                    message: error.to_string(),
                }),
            )
                .into_response(),
        }
    }
}
}

The architectural role is similar, but the implementation is driven by plain types instead of reflection and advice rules.
两者在架构职责上很像,但 Rust 这边是由普通类型驱动,而不是靠反射和 advice 规则完成。

Why #[from] Matters
为什么 #[from] 很关键

#[from] lets infrastructure errors flow upward without repetitive wrapping code.
#[from] 可以让基础设施层错误向上传播时不必每次都手工包一层。

#![allow(unused)]
fn main() {
#[derive(thiserror::Error, Debug)]
pub enum ImportError {
    #[error("I/O error: {0}")]
    Io(#[from] std::io::Error),

    #[error("CSV parse error: {0}")]
    Csv(#[from] csv::Error),

    #[error("row {row}: invalid email")]
    InvalidEmail { row: usize },
}
}

This is the Rust alternative to a long chain of catch, wrap, and rethrow blocks.
这就是 Rust 对“层层 catch、包装、再抛出”的替代方案。

thiserror vs anyhow
thiserroranyhow 的分工

Tool
工具
Best use
最佳用途
thiserrorlibrary crates, service layers, reusable modules
库 crate、服务层、可复用模块
anyhowbinaries, CLI entrypoints, one-off tools
可执行程序、CLI 入口、一次性工具

A good house rule for Java-to-Rust migration is simple:
面向 Java 迁移时,一个很好用的团队规则很简单:

  • reusable layers use thiserror
    可复用层用 thiserror
  • outer executable boundaries use anyhow
    最外层可执行边界用 anyhow

Practical Rules
实用规则

  1. keep one central error enum per crate whenever possible
    可以的话,每个 crate 保持一个中心错误枚举。
  2. use variants for domain failures that callers may distinguish
    调用方需要区分的领域失败,用独立变体表示。
  3. use #[from] for infrastructure failures that should bubble up
    需要向上传播的基础设施失败,用 #[from] 承接。
  4. convert to HTTP or CLI output only at the outer boundary
    只有在最外层边界才去翻译成 HTTP 响应或 CLI 输出。