返回 SC 笔记
SC Day 20

Rust - 错误处理深入 - 自定义Error + ? 运算符 + thiserror/anyhow

### 1. Rust 错误处理哲学

2026-04-20
第一阶段:基础构建
rusterror-handlingcustom-errorthiserroranyhowresultquestion-mark

日期: 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 错误处理的核心语法糖。它做了三件事:

  1. 如果 ResultOk(v),提取 v 继续执行
  2. 如果 ResultErr(e),调用 From::from(e) 转换错误类型,然后提前返回
  3. 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 是一个过程宏库,自动为你的错误枚举生成 DisplayError 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 决策框架

维度thiserroranyhow
适用场景库代码 / SDK应用代码 / CLI / 服务
错误类型自定义枚举,精确分类通用 anyhow::Error,包裹一切
调用方体验可以 match 每种错误只能读消息或 downcast
上下文需手动在 #[error].context() 动态添加
错误链#[source] / #[from]自动维护 .chain()
典型产出SdkError, RpcErrorResult<T> (= Result<T, anyhow::Error>)

经验法则

  • 写给别人用的代码(库/SDK)-> thiserror
  • 自己的应用/工具/脚本 -> anyhow
  • 大型项目中通常两者结合:底层库用 thiserror,应用层用 anyhow

? 运算符核心规则

规则说明
只能在返回 ResultOption 的函数中使用需要有对应的返回类型
自动调用 From::from() 转换错误类型这就是为什么 #[from] 很有用
Option 也可用None 会立即返回 None
不能混用 ResultOption?.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 项目中,错误处理的最佳实践?

答案要点

  1. 分层设计:RPC 层、解码层、交易层各有独立的错误枚举
  2. 可重试标记:在错误类型上实现 is_retryable() 方法,让调用方知道是否值得重试
  3. 保留错误链:用 #[source]anyhow::Context 保留底层原因,方便调试
  4. 避免 panicunwrap() 只在测试或可证明安全的场景使用
  5. 结构化日志:错误枚举的变体可以直接用于监控告警分类

参考资源