错误处理

在Zino框架中,我们定义了一个通用的错误类型Error,主要目的是实现以下功能:

  1. 基于字符串将任意错误包装成同一类型;
  2. 支持错误溯源,并能追溯到原始错误;
  3. 支持传递自定义错误信息上下文类型;
  4. 支持tracing,自动记录错误信息。

这四条需求对Zino框架至关重要,这也是为什么我们没有采用社区中流行的错误处理库,比如anyhow。 在实际应用开发中,我们往往并不会对具体的错误类型做不同的处理1,而是直接返回错误消息, 所以我们采取基于字符串的错误处理:

#[derive(Debug)]
pub struct Error {
    message: SharedString,
    source: Option<Box<Error>>,
    context: Option<Box<dyn Any + Send>>,
}

其中SharedString是Zino中用来优化静态字符串处理的类型2。 我们可以调用sources方法返回一个迭代器进行错误溯源,也可以使用root_source方法来追溯到原始错误。

对于任意满足Send + 'static约束的类型,我们可以将它作为错误信息上下文来传递:

pub fn set_context<T: Send + 'static>(&mut self, context: T) {
    self.context = Some(Box::new(context));
}

pub fn get_context<T: Send + 'static>(&self) -> Option<&T> {
    self.context
        .as_ref()
        .and_then(|ctx| ctx.downcast_ref::<T>())
}

对于任意实现了std::error::Error trait的错误类型,我们可以将它转换为Error类型:

impl<E: error::Error + Send + 'static> From<E> for Error {
    fn from(err: E) -> Self {
        Self {
            message: err.to_string().into(),
            source: err.source().map(|err| Box::new(Self::new(err.to_string()))),
            context: Some(Box::new(err)),
        }
    }
}

这样在需要返回Result<T, zino_core::error::Error>的函数中,我们就可以很方便地使用?运算符。 需要注意的是,我们的Error类型本身并没有实现std::error::Error

通过为Error类型实现std::fmt::Display,我们可以提供对tracing的集成,让它自动记录错误信息:

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let message = self.message();
        if let Some(source) = &self.source {
            let source = source.message();
            let root_source = self.root_source().map(|err| err.message());
            if root_source != Some(source) {
                tracing::error!(root_source, source, message);
            } else {
                tracing::error!(root_source, message);
            }
        } else {
            tracing::error!(message);
        }
        write!(f, "{message}")
    }
}

每当我们调用.to_string()时,tracing::error!就会自动生成一条记录。

1

当然,你也可以自定义可枚举的错误类型,并为其实现std::error::Error trait。

2

我们的Error类型对于静态字符串的处理有巨大的性能优势,具体可以参考我们的box_error测试。