Rust - 错误处理深入 - 自定义Error + ? 运算符 + thiserror/anyhow
### 1. Rust 错误处理哲学
日期: 2026-04-20 方向: Rust 阶段: 第一阶段:基础构建 标签: #rust #error-handling #custom-error #thiserror #anyhow #result #question-mark
今日目标
| 类型 | 内容 |
|---|---|
| 学习 | 掌握自定义 Error 枚举设计、实现 std::error::Error trait、? 运算符的工作原理、thiserror 和 anyhow 的使用场景 |
| 实操 | 实现完整的区块链客户端错误处理系统(RPC 错误/解析错误/业务错误/超时) |
| 产出 | 错误处理架构设计 + thiserror vs anyhow 决策框架 + 完整代码 + 面试题 |
核心概念
1. Rust 错误处理哲学
Rust 没有异常(exception),所有错误都通过返回值显式传递。这意味着每个可能失败的操作都必须在类型签名中体现。这看似啰嗦,但它带来了巨大的可靠性优势——你不可能"忘记"处理一个错误。
// 在 Java/Python 中,错误是"隐式"的:
// String data = httpClient.get(url); // 可能抛 IOException,但签名看不出来
// 在 Rust 中,错误是"显式"的:
// fn get(url: &str) -> Result<String, HttpError> // 返回类型明确告诉你可能失败
Rust 的核心错误类型:
// Result<T, E> —— 可能成功(Ok(T))或失败(Err(E))
enum Result<T, E> {
Ok(T),
Err(E),
}
// Option<T> —— 可能有值(Some(T))或没有(None)
enum Option<T> {
Some(T),
None,
}
2. 自定义 Error 枚举
在真实项目中,你不会只用 String 作为错误类型。好的做法是定义一个枚举,每个变体代表一种错误类别。
use std::fmt;
use std::num::ParseIntError;
/// 区块链 RPC 客户端的错误类型
#[derive(Debug)]
enum RpcError {
/// 网络连接失败
ConnectionFailed {
url: String,
reason: String,
},
/// HTTP 错误响应
HttpError {
status_code: u16,
body: String,
},
/// JSON-RPC 协议错误
JsonRpcError {
code: i64,
message: String,
},
/// 响应解析失败
ParseError(String),
/// 请求超时
Timeout {
url: String,
duration_ms: u64,
},
/// 认证失败
Unauthorized,
/// 速率限制
RateLimited {
retry_after_ms: u64,
},
}
/// 实现 Display trait —— 用于给用户看的错误消息
impl fmt::Display for RpcError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RpcError::ConnectionFailed { url, reason } => {
write!(f, "无法连接到 {}: {}", url, reason)
}
RpcError::HttpError { status_code, body } => {
write!(f, "HTTP {} 错误: {}", status_code, body)
}
RpcError::JsonRpcError { code, message } => {
write!(f, "JSON-RPC 错误 ({}): {}", code, message)
}
RpcError::ParseError(detail) => {
write!(f, "解析响应失败: {}", detail)
}
RpcError::Timeout { url, duration_ms } => {
write!(f, "请求 {} 超时 ({}ms)", url, duration_ms)
}
RpcError::Unauthorized => {
write!(f, "API Key 无效或已过期")
}
RpcError::RateLimited { retry_after_ms } => {
write!(f, "请求频率过高,请 {}ms 后重试", retry_after_ms)
}
}
}
}
/// 实现 std::error::Error trait —— 让错误可以被标准错误处理机制使用
/// source() 方法返回底层错误(如果有的话),形成错误链
impl std::error::Error for RpcError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
// 这个简单实现没有底层错误
// 如果有,可以返回 Some(inner_error)
None
}
}
fn main() {
let err = RpcError::Timeout {
url: "https://eth-mainnet.alchemyapi.io".into(),
duration_ms: 30000,
};
// Display trait 输出(面向用户)
println!("错误: {}", err);
// Debug trait 输出(面向开发者)
println!("调试: {:?}", err);
// 可以向上转型为 dyn Error
let boxed: Box<dyn std::error::Error> = Box::new(err);
println!("Box<dyn Error>: {}", boxed);
}
3. From trait:实现错误类型转换
From trait 允许一种错误类型自动转换为另一种,这是 ? 运算符能工作的基础。
use std::num::ParseIntError;
use std::fmt;
#[derive(Debug)]
enum BlockchainError {
Rpc(String),
Parse(String),
InvalidBlockNumber(ParseIntError), // 包裹标准库错误
InvalidAddress(String),
InsufficientBalance {
required: u128,
available: u128,
},
}
impl fmt::Display for BlockchainError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BlockchainError::Rpc(msg) => write!(f, "RPC 错误: {}", msg),
BlockchainError::Parse(msg) => write!(f, "解析错误: {}", msg),
BlockchainError::InvalidBlockNumber(e) => write!(f, "无效区块号: {}", e),
BlockchainError::InvalidAddress(addr) => write!(f, "无效地址: {}", addr),
BlockchainError::InsufficientBalance { required, available } => {
write!(f, "余额不足: 需要 {} wei, 只有 {} wei", required, available)
}
}
}
}
impl std::error::Error for BlockchainError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
BlockchainError::InvalidBlockNumber(e) => Some(e),
_ => None,
}
}
}
/// 实现 From<ParseIntError>,允许 ParseIntError 自动转换为 BlockchainError
impl From<ParseIntError> for BlockchainError {
fn from(err: ParseIntError) -> Self {
BlockchainError::InvalidBlockNumber(err)
}
}
/// 解析十六进制区块号(如 "0x1234")
fn parse_block_number(hex_str: &str) -> Result<u64, BlockchainError> {
let hex = hex_str.strip_prefix("0x").unwrap_or(hex_str);
// u64::from_str_radix 返回 Result<u64, ParseIntError>
// 因为我们实现了 From<ParseIntError>,? 会自动转换
let number = u64::from_str_radix(hex, 16)?;
Ok(number)
}
fn validate_address(addr: &str) -> Result<(), BlockchainError> {
if !addr.starts_with("0x") {
return Err(BlockchainError::InvalidAddress(
format!("地址必须以 0x 开头: {}", addr)
));
}
if addr.len() != 42 {
return Err(BlockchainError::InvalidAddress(
format!("地址长度应为 42, 实际为 {}: {}", addr.len(), addr)
));
}
Ok(())
}
fn main() {
// 正常情况
match parse_block_number("0x1234") {
Ok(num) => println!("区块号: {}", num), // 4660
Err(e) => println!("错误: {}", e),
}
// 错误情况:无效十六进制
match parse_block_number("0xGGGG") {
Ok(num) => println!("区块号: {}", num),
Err(e) => {
println!("错误: {}", e);
// 查看底层错误
if let Some(source) = std::error::Error::source(&e) {
println!("底层原因: {}", source);
}
}
}
// 地址验证
match validate_address("not_an_address") {
Ok(()) => println!("地址有效"),
Err(e) => println!("错误: {}", e),
}
}
4. ? 运算符深入
? 运算符是 Rust 错误处理的核心语法糖。它做了三件事:
- 如果
Result是Ok(v),提取v继续执行 - 如果
Result是Err(e),调用From::from(e)转换错误类型,然后提前返回 - 对
Option也适用——None会提前返回None
use std::collections::HashMap;
#[derive(Debug)]
enum WalletError {
NotFound(String),
InsufficientFunds { address: String, required: u128, balance: u128 },
InvalidAmount(String),
TransferFailed(String),
}
impl std::fmt::Display for WalletError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
WalletError::NotFound(addr) => write!(f, "钱包不存在: {}", addr),
WalletError::InsufficientFunds { address, required, balance } => {
write!(f, "钱包 {} 余额不足: 需要 {}, 可用 {}", address, required, balance)
}
WalletError::InvalidAmount(msg) => write!(f, "无效金额: {}", msg),
WalletError::TransferFailed(msg) => write!(f, "转账失败: {}", msg),
}
}
}
impl std::error::Error for WalletError {}
struct WalletManager {
balances: HashMap<String, u128>,
}
impl WalletManager {
fn new() -> Self {
WalletManager {
balances: HashMap::new(),
}
}
fn create_wallet(&mut self, address: &str, initial_balance: u128) {
self.balances.insert(address.to_string(), initial_balance);
}
/// 获取余额——可能失败(钱包不存在)
fn get_balance(&self, address: &str) -> Result<u128, WalletError> {
self.balances
.get(address)
.copied()
.ok_or_else(|| WalletError::NotFound(address.to_string()))
// ok_or_else 将 Option 转为 Result:
// Some(v) -> Ok(v)
// None -> Err(WalletError::NotFound(...))
}
/// 转账——多个步骤都可能失败,用 ? 链式传播
fn transfer(&mut self, from: &str, to: &str, amount: u128) -> Result<(), WalletError> {
// 验证金额
if amount == 0 {
return Err(WalletError::InvalidAmount("金额不能为零".into()));
}
// ? 在这里的工作:
// 1. get_balance 返回 Result<u128, WalletError>
// 2. 如果 Ok(balance),balance 被提取出来
// 3. 如果 Err(e),函数立即返回 Err(e)
let from_balance = self.get_balance(from)?;
let _to_balance = self.get_balance(to)?; // 确认收款方存在
// 检查余额
if from_balance < amount {
return Err(WalletError::InsufficientFunds {
address: from.to_string(),
required: amount,
balance: from_balance,
});
}
// 执行转账
*self.balances.get_mut(from).unwrap() -= amount;
*self.balances.get_mut(to).unwrap() += amount;
Ok(())
}
/// 批量转账——展示 ? 在循环中的使用
fn batch_transfer(
&mut self,
from: &str,
transfers: &[(String, u128)],
) -> Result<u128, WalletError> {
let mut total_sent: u128 = 0;
for (to, amount) in transfers {
// 任何一笔失败都会立即返回错误
// 注意:已成功的转账不会回滚(这不是数据库事务)
self.transfer(from, to, *amount)?;
total_sent += amount;
}
Ok(total_sent)
}
}
fn main() {
let mut manager = WalletManager::new();
manager.create_wallet("0xAlice", 10_000_000_000_000_000_000); // 10 ETH in wei
manager.create_wallet("0xBob", 5_000_000_000_000_000_000); // 5 ETH
manager.create_wallet("0xCarol", 1_000_000_000_000_000_000); // 1 ETH
// 正常转账
match manager.transfer("0xAlice", "0xBob", 2_000_000_000_000_000_000) {
Ok(()) => println!("转账成功"),
Err(e) => println!("转账失败: {}", e),
}
// 余额不足
match manager.transfer("0xCarol", "0xAlice", 99_000_000_000_000_000_000) {
Ok(()) => println!("转账成功"),
Err(e) => println!("转账失败: {}", e),
}
// 钱包不存在
match manager.transfer("0xUnknown", "0xAlice", 1) {
Ok(()) => println!("转账成功"),
Err(e) => println!("转账失败: {}", e),
}
// 批量转账
let transfers = vec![
("0xBob".to_string(), 1_000_000_000_000_000_000),
("0xCarol".to_string(), 500_000_000_000_000_000),
];
match manager.batch_transfer("0xAlice", &transfers) {
Ok(total) => println!("批量转账成功,总计: {} wei", total),
Err(e) => println!("批量转账失败: {}", e),
}
// 打印最终余额
for addr in &["0xAlice", "0xBob", "0xCarol"] {
println!("{}: {} wei", addr, manager.get_balance(addr).unwrap());
}
}
? 对 Option 的使用
#[derive(Debug)]
struct BlockHeader {
number: u64,
hash: String,
parent_hash: String,
timestamp: u64,
}
struct BlockStore {
blocks: Vec<BlockHeader>,
}
impl BlockStore {
/// 获取第 n 个区块的父区块的时间戳
/// 多层嵌套的 Option 用 ? 优雅处理
fn get_parent_timestamp(&self, block_number: u64) -> Option<u64> {
// 找到指定区块
let block = self.blocks.iter().find(|b| b.number == block_number)?;
// ? 在 Option 上: None 直接返回 None, Some(v) 提取 v
// 找到父区块
let parent = self.blocks.iter().find(|b| b.hash == block.parent_hash)?;
Some(parent.timestamp)
}
/// 等价的不用 ? 的写法(嵌套地狱)
fn get_parent_timestamp_verbose(&self, block_number: u64) -> Option<u64> {
match self.blocks.iter().find(|b| b.number == block_number) {
Some(block) => {
match self.blocks.iter().find(|b| b.hash == block.parent_hash) {
Some(parent) => Some(parent.timestamp),
None => None,
}
}
None => None,
}
}
}
5. thiserror:优雅定义库级别的错误
thiserror 是一个过程宏库,自动为你的错误枚举生成 Display 和 Error trait 实现。它适用于库代码,让调用方能精确匹配和处理不同类型的错误。
// Cargo.toml:
// [dependencies]
// thiserror = "2"
use thiserror::Error;
/// 使用 thiserror 定义错误——代码量减少 70%
#[derive(Error, Debug)]
enum ChainClientError {
/// #[error("...")] 自动生成 Display 实现
#[error("连接 RPC 节点失败: {url} - {reason}")]
ConnectionFailed {
url: String,
reason: String,
},
/// {0} 引用元组结构体的第一个字段
#[error("HTTP 请求失败: 状态码 {0}")]
HttpError(u16),
/// #[from] 自动生成 From 实现
#[error("JSON 解析失败")]
JsonError(#[from] serde_json::Error),
/// 嵌套错误,#[source] 标记底层错误(用于错误链)
#[error("IO 错误: {context}")]
IoError {
context: String,
#[source]
source: std::io::Error,
},
#[error("无效的区块号: {0}")]
InvalidBlockNumber(#[from] std::num::ParseIntError),
#[error("交易未找到: {hash}")]
TransactionNotFound { hash: String },
#[error("合约调用 revert: {reason}")]
ContractRevert { reason: String },
#[error("请求超时: {0}ms")]
Timeout(u64),
/// transparent 把内部错误直接暴露,不添加额外上下文
#[error(transparent)]
Other(#[from] Box<dyn std::error::Error + Send + Sync>),
}
// ============ 使用示例 ============
// 模拟 serde_json 类型
// (实际项目中引入 serde_json crate)
mod serde_json {
#[derive(Debug)]
pub struct Error;
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "JSON parse error")
}
}
impl std::error::Error for Error {}
}
/// 模拟获取区块数据
fn fetch_block(number: &str) -> Result<String, ChainClientError> {
// ? 自动调用 From<ParseIntError> -> ChainClientError::InvalidBlockNumber
let block_num = u64::from_str_radix(
number.strip_prefix("0x").unwrap_or(number), 16
)?;
if block_num > 20_000_000 {
return Err(ChainClientError::ConnectionFailed {
url: "https://rpc.example.com".into(),
reason: "区块号超出范围".into(),
});
}
Ok(format!("Block #{}", block_num))
}
/// 模拟获取交易
fn fetch_transaction(hash: &str) -> Result<String, ChainClientError> {
if hash == "0x0000" {
return Err(ChainClientError::TransactionNotFound {
hash: hash.to_string(),
});
}
Ok(format!("Tx {}", hash))
}
fn main() {
// 正常情况
match fetch_block("0xFF") {
Ok(block) => println!("成功: {}", block),
Err(e) => println!("失败: {}", e),
}
// ParseIntError 自动转换
match fetch_block("0xZZZZ") {
Ok(block) => println!("成功: {}", block),
Err(e) => {
println!("失败: {}", e);
// 访问错误链
let mut source = std::error::Error::source(&e);
while let Some(err) = source {
println!(" 原因: {}", err);
source = std::error::Error::source(err);
}
}
}
// TransactionNotFound
match fetch_transaction("0x0000") {
Ok(tx) => println!("成功: {}", tx),
Err(ChainClientError::TransactionNotFound { hash }) => {
println!("交易 {} 未找到,可能还在 mempool 中", hash);
}
Err(e) => println!("其他错误: {}", e),
}
}
thiserror 的关键特性:
#[error("...")]自动生成Display#[from]自动生成From实现(使?可以自动转换)#[source]标记错误链中的底层错误#[error(transparent)]把内部错误直接暴露
6. anyhow:优雅的应用级错误处理
anyhow 适用于应用代码(而非库代码)。它提供了一个通用的 anyhow::Error 类型,可以包裹任何实现了 std::error::Error 的类型,加上上下文信息。
// Cargo.toml:
// [dependencies]
// anyhow = "1"
// thiserror = "2"
use anyhow::{anyhow, bail, Context, Result};
// 用 thiserror 定义的库级别错误(可能来自第三方库)
#[derive(thiserror::Error, Debug)]
enum RpcError {
#[error("连接失败: {0}")]
ConnectionFailed(String),
#[error("请求超时")]
Timeout,
}
#[derive(thiserror::Error, Debug)]
enum ParseError {
#[error("无效的十六进制: {0}")]
InvalidHex(String),
#[error("无效的 ABI 编码")]
InvalidAbi,
}
// ============ 应用代码使用 anyhow ============
/// 应用函数返回 anyhow::Result<T>
/// 等价于 Result<T, anyhow::Error>
fn get_eth_balance(address: &str) -> Result<f64> {
// 验证地址
if !address.starts_with("0x") || address.len() != 42 {
// bail! 宏:创建错误并立即返回
bail!("无效的以太坊地址: {}", address);
}
// .context() 为错误添加上下文信息
let response = simulate_rpc_call(address)
.context("查询余额时 RPC 调用失败")?;
// 如果 simulate_rpc_call 返回 Err,错误链变成:
// "查询余额时 RPC 调用失败" -> 原始 RpcError
let balance = parse_balance(&response)
.context("解析余额响应失败")?;
Ok(balance)
}
fn simulate_rpc_call(address: &str) -> std::result::Result<String, RpcError> {
if address.contains("dead") {
return Err(RpcError::ConnectionFailed("节点无响应".into()));
}
Ok(r#"{"result": "0x8AC7230489E80000"}"#.to_string())
}
fn parse_balance(response: &str) -> std::result::Result<f64, ParseError> {
// 简化实现
if response.contains("0x8AC7230489E80000") {
Ok(10.0) // 10 ETH
} else {
Err(ParseError::InvalidHex(response.to_string()))
}
}
/// 批量查询余额——展示 anyhow 的错误聚合
fn scan_wallets(addresses: &[&str]) -> Result<Vec<(String, f64)>> {
let mut results = Vec::new();
for (i, addr) in addresses.iter().enumerate() {
let balance = get_eth_balance(addr)
.with_context(|| format!("查询第 {} 个钱包 {} 失败", i + 1, addr))?;
// with_context 接受闭包,只在错误时才构造上下文字符串(性能更好)
results.push((addr.to_string(), balance));
}
Ok(results)
}
/// 展示 anyhow! 宏创建临时错误
fn check_chain_id(expected: u64) -> Result<()> {
let actual: u64 = 1; // 模拟获取 chain ID
if actual != expected {
// anyhow! 宏:创建一个 anyhow::Error
return Err(anyhow!(
"链 ID 不匹配: 期望 {}, 实际 {}. 请检查钱包网络设置",
expected, actual
));
}
Ok(())
}
fn main() {
println!("=== 正常查询 ===");
match get_eth_balance("0x1234567890abcdef1234567890abcdef12345678") {
Ok(balance) => println!("余额: {} ETH", balance),
Err(e) => {
// anyhow 提供了完整的错误链打印
println!("错误: {}", e);
// 打印完整错误链
println!("\n完整错误链:");
for (i, cause) in e.chain().enumerate() {
println!(" {}: {}", i, cause);
}
}
}
println!("\n=== 无效地址 ===");
match get_eth_balance("not_an_address") {
Ok(_) => unreachable!(),
Err(e) => println!("错误: {}", e),
}
println!("\n=== 连接失败 ===");
match get_eth_balance("0xdead567890abcdef1234567890abcdef12345678") {
Ok(_) => unreachable!(),
Err(e) => {
println!("错误: {}", e);
println!("错误链:");
for (i, cause) in e.chain().enumerate() {
println!(" {}: {}", i, cause);
}
}
}
println!("\n=== 批量扫描 ===");
let wallets = vec![
"0x1234567890abcdef1234567890abcdef12345678",
"0xdead567890abcdef1234567890abcdef12345678", // 这个会失败
];
match scan_wallets(&wallets) {
Ok(results) => {
for (addr, balance) in results {
println!(" {}: {} ETH", addr, balance);
}
}
Err(e) => {
println!("扫描失败:");
for cause in e.chain() {
println!(" - {}", cause);
}
}
}
println!("\n=== 链 ID 检查 ===");
match check_chain_id(137) {
Ok(()) => println!("链 ID 正确"),
Err(e) => println!("错误: {}", e),
}
}
代码实战:完整的区块链客户端错误处理架构
将 thiserror 和 anyhow 结合,构建一个分层的错误处理系统。
// ============================================================
// 第一层:底层库错误(用 thiserror)
// 各个模块定义自己的精确错误类型
// ============================================================
use thiserror::Error;
/// RPC 通信层错误
#[derive(Error, Debug)]
pub enum RpcError {
#[error("连接到 {url} 失败: {reason}")]
ConnectionFailed { url: String, reason: String },
#[error("HTTP {status}: {body}")]
HttpError { status: u16, body: String },
#[error("请求超时 ({timeout_ms}ms)")]
Timeout { timeout_ms: u64 },
#[error("JSON-RPC 错误 ({code}): {message}")]
JsonRpc { code: i64, message: String },
#[error("速率限制,{retry_after_ms}ms 后重试")]
RateLimited { retry_after_ms: u64 },
}
/// 数据解析层错误
#[derive(Error, Debug)]
pub enum DecodeError {
#[error("无效的十六进制字符串: {0}")]
InvalidHex(String),
#[error("ABI 解码失败: 期望 {expected} 字节, 得到 {actual} 字节")]
AbiDecodeFailed { expected: usize, actual: usize },
#[error("无效的函数选择器: {0}")]
InvalidSelector(String),
#[error("数值溢出: {0}")]
Overflow(String),
}
/// 交易构建层错误
#[derive(Error, Debug)]
pub enum TxBuildError {
#[error("Gas 估算失败: {0}")]
GasEstimationFailed(String),
#[error("Nonce 过低: 期望 >= {expected}, 使用了 {used}")]
NonceTooLow { expected: u64, used: u64 },
#[error("余额不足: 需要 {required_wei} wei, 可用 {available_wei} wei")]
InsufficientBalance { required_wei: u128, available_wei: u128 },
#[error("交易被 revert: {reason}")]
Reverted { reason: String },
}
// ============================================================
// 第二层:聚合错误(用 thiserror 组合底层错误)
// 供 SDK 的公共 API 使用
// ============================================================
/// SDK 公共 API 的错误类型
#[derive(Error, Debug)]
pub enum SdkError {
#[error("RPC 通信错误")]
Rpc(#[from] RpcError),
#[error("数据解码错误")]
Decode(#[from] DecodeError),
#[error("交易构建错误")]
TxBuild(#[from] TxBuildError),
#[error("钱包错误: {0}")]
Wallet(String),
#[error("配置错误: {0}")]
Config(String),
}
impl SdkError {
/// 判断错误是否可重试
pub fn is_retryable(&self) -> bool {
match self {
SdkError::Rpc(RpcError::Timeout { .. }) => true,
SdkError::Rpc(RpcError::RateLimited { .. }) => true,
SdkError::Rpc(RpcError::ConnectionFailed { .. }) => true,
SdkError::TxBuild(TxBuildError::NonceTooLow { .. }) => true,
_ => false,
}
}
/// 获取重试等待时间
pub fn retry_after_ms(&self) -> Option<u64> {
match self {
SdkError::Rpc(RpcError::RateLimited { retry_after_ms }) => Some(*retry_after_ms),
SdkError::Rpc(RpcError::Timeout { .. }) => Some(1000),
_ => None,
}
}
}
// ============================================================
// 第三层:应用层(用 anyhow 添加上下文)
// ============================================================
use anyhow::{Context, Result};
/// 模拟 SDK 函数
fn sdk_get_balance(address: &str) -> std::result::Result<u128, SdkError> {
if address.contains("bad") {
return Err(SdkError::Rpc(RpcError::ConnectionFailed {
url: "https://rpc.example.com".into(),
reason: "DNS 解析失败".into(),
}));
}
Ok(10_000_000_000_000_000_000) // 10 ETH
}
fn sdk_send_transaction(
from: &str,
to: &str,
value: u128,
) -> std::result::Result<String, SdkError> {
let balance = sdk_get_balance(from)?; // SdkError 自动传播
if balance < value {
return Err(SdkError::TxBuild(TxBuildError::InsufficientBalance {
required_wei: value,
available_wei: balance,
}));
}
Ok("0xtxhash123".to_string())
}
/// 应用层:使用 anyhow 添加业务上下文
fn app_execute_swap(
user_address: &str,
token_in: &str,
token_out: &str,
amount: u128,
) -> Result<String> {
// 检查余额
let balance = sdk_get_balance(user_address)
.context(format!("检查用户 {} 余额", user_address))?;
println!("用户余额: {} wei", balance);
// 执行交易
let tx_hash = sdk_send_transaction(user_address, "0xUniswapRouter", amount)
.with_context(|| {
format!(
"执行 Swap 失败: {} {} -> {} (金额: {} wei)",
user_address, token_in, token_out, amount
)
})?;
Ok(tx_hash)
}
/// 应用层:带重试逻辑的执行
fn app_execute_with_retry(
user_address: &str,
max_retries: u32,
) -> Result<String> {
let mut last_error = None;
for attempt in 1..=max_retries {
match sdk_send_transaction(user_address, "0xTarget", 1_000_000_000) {
Ok(hash) => return Ok(hash),
Err(e) => {
if e.is_retryable() {
let wait = e.retry_after_ms().unwrap_or(1000);
println!(
"尝试 {}/{} 失败 ({}), {}ms 后重试...",
attempt, max_retries, e, wait
);
// 实际项目中这里会 sleep
last_error = Some(e);
continue;
} else {
// 不可重试的错误,立即返回
return Err(e).context(format!(
"第 {} 次尝试遇到不可重试的错误",
attempt
));
}
}
}
}
Err(last_error.unwrap())
.context(format!("{}次重试后仍然失败", max_retries))
}
fn main() {
println!("=== 正常 Swap ===");
match app_execute_swap("0xAlice", "ETH", "USDC", 1_000_000_000) {
Ok(hash) => println!("交易成功: {}", hash),
Err(e) => {
println!("Swap 失败:");
for cause in e.chain() {
println!(" -> {}", cause);
}
}
}
println!("\n=== 连接失败的 Swap ===");
match app_execute_swap("0xbad_address_here", "ETH", "USDC", 1_000_000_000) {
Ok(hash) => println!("交易成功: {}", hash),
Err(e) => {
println!("Swap 失败:");
for cause in e.chain() {
println!(" -> {}", cause);
}
}
}
println!("\n=== 带重试的执行 ===");
match app_execute_with_retry("0xbad_node_addr", 3) {
Ok(hash) => println!("最终成功: {}", hash),
Err(e) => {
println!("最终失败:");
for cause in e.chain() {
println!(" -> {}", cause);
}
}
}
}
关键要点总结
thiserror vs anyhow 决策框架
| 维度 | thiserror | anyhow |
|---|---|---|
| 适用场景 | 库代码 / SDK | 应用代码 / CLI / 服务 |
| 错误类型 | 自定义枚举,精确分类 | 通用 anyhow::Error,包裹一切 |
| 调用方体验 | 可以 match 每种错误 | 只能读消息或 downcast |
| 上下文 | 需手动在 #[error] 写 | .context() 动态添加 |
| 错误链 | #[source] / #[from] | 自动维护 .chain() |
| 典型产出 | SdkError, RpcError | Result<T> (= Result<T, anyhow::Error>) |
经验法则:
- 写给别人用的代码(库/SDK)-> thiserror
- 自己的应用/工具/脚本 -> anyhow
- 大型项目中通常两者结合:底层库用 thiserror,应用层用 anyhow
? 运算符核心规则
| 规则 | 说明 |
|---|---|
只能在返回 Result 或 Option 的函数中使用 | 需要有对应的返回类型 |
自动调用 From::from() 转换错误类型 | 这就是为什么 #[from] 很有用 |
对 Option 也可用 | None 会立即返回 None |
不能混用 Result 和 Option 的 ? | 用 .ok_or() 或 .ok() 转换 |
常见误区
误区 1:到处用 unwrap()
// 新手写法:到处 unwrap,遇到错误就 panic
fn bad_code() {
let balance = get_balance("0xAddr").unwrap(); // 生产环境崩溃!
let parsed = "abc".parse::<u64>().unwrap(); // panic!
}
// 正确做法:传播错误,在最顶层统一处理
fn good_code() -> Result<()> {
let balance = get_balance("0xAddr")?;
let parsed: u64 = "abc".parse().context("解析区块号失败")?;
Ok(())
}
// unwrap 唯一合理的使用场景:
// 1. 测试代码中
// 2. 你能证明不会失败的地方(用 expect 说明原因)
let home = std::env::var("HOME").expect("HOME 环境变量必须设置");
误区 2:用 String 作为错误类型
// 不好:String 没有类型信息,调用方无法区分错误种类
fn bad_api() -> Result<(), String> {
Err("something went wrong".to_string())
}
// 好:用枚举让调用方能精确匹配
fn good_api() -> Result<(), MyError> {
Err(MyError::NotFound { id: 42 })
}
误区 3:过度使用 anyhow 导致错误信息不可操作
// 不好:只有消息,没有结构化数据,无法程序化处理
fn bad_retry_logic() -> anyhow::Result<()> {
// 调用方如何知道应该等多久再重试?
Err(anyhow::anyhow!("rate limited"))
}
// 好:底层用 thiserror 保留结构化信息
#[derive(thiserror::Error, Debug)]
enum ApiError {
#[error("rate limited, retry after {retry_ms}ms")]
RateLimited { retry_ms: u64 },
}
// 应用层再用 anyhow 包裹
fn good_retry_logic() -> anyhow::Result<()> {
Err(ApiError::RateLimited { retry_ms: 5000 })?
// 调用方可以 downcast 获取 retry_ms
}
误区 4:忽略错误链
// 不好:丢失了底层原因
fn bad_wrapping() -> Result<(), MyError> {
let result = some_io_operation();
match result {
Err(e) => Err(MyError::IoFailed(e.to_string())), // 只保留了消息
Ok(v) => Ok(v),
}
}
// 好:保留完整的错误链
#[derive(thiserror::Error, Debug)]
enum MyError {
#[error("IO 操作失败: {context}")]
IoFailed {
context: String,
#[source] // 保留底层错误
source: std::io::Error,
},
}
面试关联
Q1: Rust 的错误处理和 Go/Java/Python 有什么区别?
核心答案:Rust 使用 Result<T, E> 类型系统而非异常机制。所有可能失败的操作都在函数签名中显式声明,编译器强制你处理每个 Result。相比之下:
- Go 也用返回值
(value, error),但不强制检查(可以_ = err),且没有?语法糖 - Java 有 checked exception,但开发者经常空 catch 或向上抛 RuntimeException 绕过
- Python 完全依靠运行时异常,签名中看不出可能的失败
Rust 的优势是零成本(无异常栈展开开销)且编译期保证完备性。? 运算符让错误传播和 try-catch 一样简洁。
Q2: 什么时候用 thiserror,什么时候用 anyhow?
核心答案:thiserror 用于库代码(对外提供精确的错误类型,让调用方能 match 处理不同情况),anyhow 用于应用代码(快速添加上下文信息,不需要定义精确类型)。大型项目中两者结合使用——底层模块用 thiserror 定义结构化错误,应用入口用 anyhow 聚合并添加业务上下文。
Q3: 区块链 Rust 项目中,错误处理的最佳实践?
答案要点:
- 分层设计:RPC 层、解码层、交易层各有独立的错误枚举
- 可重试标记:在错误类型上实现
is_retryable()方法,让调用方知道是否值得重试 - 保留错误链:用
#[source]或anyhow::Context保留底层原因,方便调试 - 避免 panic:
unwrap()只在测试或可证明安全的场景使用 - 结构化日志:错误枚举的变体可以直接用于监控告警分类
参考资源
| 资源 | 说明 |
|---|---|
| The Rust Book - Error Handling | 官方教程 |
| thiserror crate | 库级别错误处理 |
| anyhow crate | 应用级别错误处理 |
| Rust Error Handling - BurntSushi | 经典长文 |
| Error Handling in a Correctness-Critical Rust Project | sled 数据库的实践 |
| RustConf 2020 - Error handling Isn't All About Errors | Jane Lusby 演讲 |