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
AppErrorplusAppResult<T>keeps code readable, and how this pattern replaces scattered exception classes in Rust services.
本章将学习: Java 的异常习惯如何迁移到 Rust 的 crate 级错误枚举,AppError加AppResult<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
thiserror 和 anyhow 的分工
| Tool 工具 | Best use 最佳用途 |
|---|---|
thiserror | library crates, service layers, reusable modules 库 crate、服务层、可复用模块 |
anyhow | binaries, 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
实用规则
- keep one central error enum per crate whenever possible
可以的话,每个 crate 保持一个中心错误枚举。 - use variants for domain failures that callers may distinguish
调用方需要区分的领域失败,用独立变体表示。 - use
#[from]for infrastructure failures that should bubble up
需要向上传播的基础设施失败,用#[from]承接。 - convert to HTTP or CLI output only at the outer boundary
只有在最外层边界才去翻译成 HTTP 响应或 CLI 输出。