Rust Error Handling Notes

23-07-22 编程 #rust #notes

初学 rust 的时候,上手写代码总是遇到很多不一样的 rust 的Result类型,不同 crate 中的函数返回的Result<T, E>E都不一样,刚开始都是unwrap或者expect来处理。如果使用try!或者?的话总是编译不通过,还是对 Error 转换和处理不熟练。

Error 转换

在一个方法中,调用不同的函数会返回不同的 error 类型,需要你将这些类型转换成统一的自定义 error 类型再返回。你有以下几种途径

使用 map_err

fn cook_pasta() -> Result<Pasta, CookingError> {
    let water = boil_water().map_err(|_| CookingError::BoilWaterError)?;
    let pasta = add_pasta(&water).map_err(|_| CookingError::AddPastaError)?;
    Ok(pasta)
}
// 通过 map_err 将 boil_water() 和 add_pasta(&water) 返回的 error 都转换成了 CookingError 类型

使用 std::error::Error+From trait

定义自己的 Error 类型并实现 From trait。From trait 用于将 boil_water() 和 add_pasta(&water) 的 error 转换成自定义的 Error。其实就是将map_err的逻辑移动到 From trait 中实现,使得方法调用处看起来更简洁。

pub enum CookingError{
    BoilWaterError(String),
    AddPastaError
}
impl std::error::Error for CookingError{
    // ...
}
impl Display for CookingError{
    // ...
}
// 假设 boil_water 返回的 error 是 NoWaterError
impl From<NoWaterError> for CookingError {
 fn from(s: NoWaterError) -> Self {
        CookingError::BoilWaterError(s)
    }
}
// 假设 add_pasta 返回的 error 是 IoError
impl From<IoError> for CustomError {
    fn from(s: std::io::Error) -> Self {
        CookingError::AddPastaError(s)
    }
}
// 无需 map_err
fn cook_pasta() -> Result<Pasta, CookingError> {
    let water = boil_water()?; // 如果抛出 NoWaterError,自动转成 CookingError::BoilWaterError,下面同理
    let pasta = add_pasta(&water)?;
    Ok(pasta)
}

thiserror

thiserror 可以看作是定义 Error 的一个工具,它只帮你生成一些定义 Error 的代码,别的什么都不做,相当纯粹。如果你在开发一个 crate,那么建议使用 thiserror。

fn render() -> Result<String, std::io::Error> {
  let file = std::env::var("MARKDOWN")?;
  let source = read_to_string(file)?;
  Ok(source)
}

上面的代码无法通过编译,因为env::var() 返回的是 std::env::VarError,而 read_to_string() 返回的是 std::io::Error
为了满足 render 函数的签名,我们就需要将 env::VarError 和 io::Error 归一化为同一种错误类型。要实现这个目的有三种方式:

use std::fs::read_to_string;

fn main() -> Result<(), MyError> {
  let html = render()?;
  println!("{}", html);
  Ok(())
}

fn render() -> Result<String, MyError> {
  let file = std::env::var("MARKDOWN")?;
  let source = read_to_string(file)?;
  Ok(source)
}

#[derive(thiserror::Error, Debug)]
enum MyError {
  #[error("Environment variable not found")]
  EnvironmentVariableNotFound(#[from] std::env::VarError),
  #[error(transparent)]
  IOError(#[from] std::io::Error),
}

thiserror提供#[from] #[error]等注解简化错误类型自定义工作。

#[derive(Error)]
{
    // Attributes available to this derive:
    #[backtrace] // 
    #[error]
    #[from]
    #[source]
}

error(transparent) 表示转发底层 error 的相关信息,不修改 source 和 Display 相关方法。

anyhow

anyhow 为你定义好了一个 Error 类型,基本可以看作是一个 Box ,同时还提供了一些如 context 等扩展功能,用起来更加无脑。如果你在开发一个业务 app,建议使用 anyhow 更加方便。

use anyhow::Result;

fn main() -> Result<()> {
    let html = render()?;
    println!("{}", html);
    Ok(())
}

fn render() -> Result<String> {
    let file = std::env::var("MARKDOWN")?;
    let source = read_to_string(file).with_context(|| format!("read string from {} failed", &file))?
    Ok(source)
}

可以看到这里使用的是Result<String>,实际上这是anyhow的 type alias:pub type Result<T, E = Error> = core::result::Result<T, E>;

anyhow 还提供了with_context给 error 添加信息,看上去和 expect 类似,只不过那是 panic。

对于一个 app 服务,一些核心登录等功能可能也需要自定义 error 类型,这时可以将anyhow::Error作为其中一种 error 类型,即 thiserror + anyhow:

#[derive(Error, Debug)]
pub enum AppError {
    ...

    #[error(transparent)]
    Other(#[from] anyhow::Error),  // source and Display delegate to anyhow::Error
}

参考:
rust by example - 处理多种错误类型
Option 和 Result 的一些方法
rust doc Error Handling
简谈 Rust 中的错误处理
细说 rust 错误处理
?在 Result 中的使用
蚂蚁集团 CeresDB 团队 | 关于 Rust 错误处理的思考