Rust 中的错误处理

刘家财

2024-04-13. PDF

About ME

1. 错误类型

  • 不恢复:panic
    • 数组访问越界
  • 可以恢复:Result<T, E>
    • std.fmt.Error
    • std.io.Error

1.1. Result

enum Result<T, E> {
    Ok(T),
    Err(E),
}

fn is_ok(&self) -> bool;
fn is_err(&self) -> bool;
fn unwrap(self) -> T;
fn unwrap_err(self) -> E;
fn map(&self, op: F) -> Result<U, E>
   where F: FnOnce(T) -> U;
fn write_message() -> io::Result<()> {
    // ? 表示如果出错,立刻返回
    let mut file = File::create("valuable_data.txt")?;
    file.write_all(b"important message")?;
    Ok(())
}

match write_message() {
    Ok(happy) => println!("😄"),
    Err(sad) => {
        eprintln!("🥶");
        return;
    }
}

1.2. Error

pub trait Error: Debug + Display {
    fn source(&self) -> Option<&(dyn Error + 'static)>;
    ...
}

// 处理 error 链
fn print_error(mut error: &dyn Error) {
    println!("Got an error:{:?}", error.to_string());
    while let Some(cause) = error.source() {
        println!("Caused by: {}", cause);
        error = cause;
    }
}

1.3. Result VS Exception

  • Result,值
    • Rust、Go、Zig
  • Exception,控制流
    • Java、C++、Python、JavaScript

Exception

try {
    v1 = get_from_db();
    v2 = prepare(v1);
    v3 = render(v2);
} catch (ex) {
    // handle ex
}

Result

v1 = get_from_db()?;
v2 = prepare(v1)?;
v3 = render(v2)?;

2. 生态

anyhow

提供 anyhow::Error 类型,相比 StdError

  • 生产详细的错误链信息
  • 生成堆栈
  • 提供 From<Error> for StdError
  • 上手程度:简单 😁
fn read_file() -> anyhow::Result<String> {
    let body = std::fs::read_to_string("input.txt")
        .context("read input.txt failed")?;
    Ok(body)
}
if let Err(err) = read_file() {
    println!("{:?}", err);
}

read input.txt failed

Caused by:
    No such file or directory (os error 2)

RUST_BACKTRACE=1 cargo run

Caused by:
    No such file or directory (os error 2)

Stack backtrace:
   2: std::backtrace::Backtrace::create
             at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/std/src/backtrace.rs:331:13
   3: <E as anyhow::context::ext::StdError>::ext_context
             at /Users/jiacai/.cargo/registry/src/mirrors.tuna.tsinghua.edu.cn-df7c3c540f42cdbd/anyhow-1.0.82/src/context.rs:27:29
   4: anyhow::context::<impl anyhow::Context<T,E> for core::result::Result<T,E>>::context
             at /Users/jiacai/.cargo/registry/src/mirrors.tuna.tsinghua.edu.cn-df7c3c540f42cdbd/anyhow-1.0.82/src/context.rs:54:31
   5: anyhow::read_file
             at ./src/anyhow_demo.rs:8:16
   6: anyhow::main
             at ./src/anyhow_demo.rs:53:21

thiserror

  • 提供一个过程宏,方便用户定义自己的 Error 类型
  • 生成堆栈
  • 上手程度:中等 🙂
#[derive(Error, Debug)]
pub enum MyError {
    #[error("data store disconnected")]
    Io(#[from] io::Error),
    #[error("Unxpected error, `{0}`.")]
    Unexpected(String),
    #[error("unknown error")]
    Unknown,
}

pub fn read_file() -> Result<String, MyError> {
    let body = std::fs::read_to_string("input.txt")?;
    Ok(body)
}

snafu

类似于相当于 anyhow 与 thiserror 的结合体

  • 提供 Whatever 错误类型,简化处理

    fn read_file(path: &str) -> Result<String, Whatever> {
        std::fs::read_to_string(path)
            .with_whatever_context(|_| format!("Could not read file {path}"))
    }
    
  • 提供过程宏用于地定义错误类型
  • 生成堆栈
  • 上手程度:复杂 🙄
#[derive(Snafu, Debug)]
pub enum MyError {
    #[snafu(display("Io error, {source}"))]
    Io { source: std::io::Error },
    #[snafu(display("Unxpected error, msg:{msg}."))]
    Unexpected { msg: String },
    #[snafu(display("Unknown error"))]
    Unknown,
}

pub fn read_file() -> Result<String> {
    let body = read_to_string("input.txt").context(IoSnafu {})?;
    Ok(body)
}

3. 最佳实践

3.1. 设计理念

错误会被怎么使用?

  • 编程方式(Library)
    • 使用者会检查错误,因此其内部结构需要合理地暴露出来
    • thiserror、Snafu
  • 显示给用户(Binary)

    使用者不会检查 fmt::Display 以外的错误信息,因此可以封装其内部结构

    • anyhow、Snafu

3.2. 基于 Enum 设计的常见问题

pub enum Error {
  Tokio(tokio::io::Error),
  ConnectionDiscovery {
    path: PathBuf,
    reason: String,
    stderr: String,
  },
  Deserialize {
    source: serde_json::Error,
    data: String,
  },
  ...,
  Generic(String),
}

可能有的问题?

  • 暴露实现细节
  • 体积过大

std.io.Error


pub struct Error {
  repr: Repr,
}

enum Repr {
  Os(i32),
  Simple(ErrorKind),
  Custom(Box<Custom>),
}

struct Custom {
  kind: ErrorKind,
  error: Box<dyn error::Error + Send + Sync>,
}
#[derive(Clone, Copy)]
#[non_exhaustive]
pub enum ErrorKind {
  NotFound,
  PermissionDenied,
  Other,
}
impl Error {
  pub fn kind(&self) -> ErrorKind {
    match &self.repr {
      Repr::Os(code) => sys::decode_error_kind(*code),
      Repr::Custom(c) => c.kind,
      Repr::Simple(kind) => *kind,
    }
  }
}
anyhow 👍
thiserror 👍
snafu 👎

4. 扩展阅读

Thanks

Q & A

https://horaedb.apache.org/