错误处理
在Zino框架中,我们定义了一个通用的错误类型Error
,主要目的是实现以下功能:
- 基于字符串将任意错误包装成同一类型;
- 支持错误溯源,并能追溯到原始错误;
- 支持传递自定义错误信息上下文类型;
- 支持
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。