错误处理

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

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

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

  1. #[derive(Debug)]
  2. pub struct Error {
  3. message: SharedString,
  4. source: Option<Box<Error>>,
  5. }

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

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

  1. impl<E: std::error::Error> From<E> for Error {
  2. #[inline]
  3. fn from(err: E) -> Self {
  4. Self {
  5. message: err.to_string().into(),
  6. source: err.source().map(|err| Box::new(Self::new(err.to_string()))),
  7. }
  8. }
  9. }

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

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

  1. impl std::fmt::Display for Error {
  2. fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  3. let message = self.message();
  4. if let Some(source) = &self.source {
  5. let source = source.message();
  6. let root_source = self.root_source().map(|err| err.message());
  7. if root_source != Some(source) {
  8. tracing::error!(root_source, source, message);
  9. } else {
  10. tracing::error!(root_source, message);
  11. }
  12. } else {
  13. tracing::error!(message);
  14. }
  15. write!(f, "{message}")
  16. }
  17. }

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

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