「深挖Rust」错误处理 — 2
「这是我参与2022首次更文挑战的第 3 天,活动详情查看:2022首次更文挑战」。
Troubleshoot
如果一个操作只有一种失败模式怎么办,我们应该只用 ()
作为错误类型吗?
Err(())
可能足以让调用者决定做什么。例如,向用户返回一个500内部服务器错误。
但是控制流并不是应用程序中错误的唯一目的。我们希望错误能够携带足够的关于故障的上下文,以便让开发人员生成一份 *“报告” *,其中包含足够的细节,以便去解决这个问题。
我们所说的 *“报告” *是什么意思?
- 在像我们这样的后端API中,它通常是一个日志。
- 在CLI中,当使用
--verbose
标志时,它可能是一个显示在终端的错误信息。
实现的细节可能有所不同,但目的是相同的:那就是帮助开发者了解什么地方出了什么问题。这正是我们在初始代码段中所做的。
//! src/routes/subscriptions.rs
// [...]
pub async fn store_token(/* */) -> Result<(), sqlx::Error> {
sqlx::query!(/* */)
.execute(transaction)
.await
.map_err(|e| {
tracing::error!("Failed to execute query: {:?}", e);
e
})?;
// [...]
}
如果查询失败,我们会抓取错误并生成一条日志,之后我们想追溯这个问题,就可以在调查数据库问题时去检查错误日志。
边缘错误
到目前为止,我们关注的是我们的API的内部,即:函数调用其他函数,以及操作者试图在错误发生后弄清其中的含义。但是用户呢?
就像操作者一样,用户希望API在遇到错误时能发出信号。
当 store_token()
失败时,API的用户会看到什么?我们看看请求处理 handler
:
//! src/routes/subscriptions.rs
// [...]
pub async fn subscribe(/* */) -> HttpResponse {
// [...]
if store_token(&mut transaction, subscriber_id, &subscription_token)
.await
.is_err()
{
return HttpResponse::InternalServerError().finish();
}
// [...]
}
他们会收到一个没有正文的HTTP响应和一个500内部服务器错误的状态码。
状态码的作用与 store_token()
中的错误类型相同:它是一个机器可解析的信息,调用者(如浏览器)可以用它来决定下一步该怎么做(例如,假设这是一个短暂的失败,请求重试即可)。
那么,浏览器背后的用户呢?我们要告诉他们什么?
什么都没有,响应体是空的。这实际上是一个很好的实现:用户不应该关心他们正在调用的API的内部情况。他们没有关于它的预期模型,也没有办法确定它为什么失败。那是操作者的范畴,我们在设计上省略了这些细节。
而在其他情况下,我们需要向用户传达额外的信息。让我们看看我们对同一个终端的输入验证:
//! src/routes/subscriptions.rs
#[derive(serde::Deserialize)]
pub struct FormData {
email: String,
name: String,
}
impl TryFrom<FormData> for NewSubscriber {
type Error = String;
fn try_from(value: FormData) -> Result<Self, Self::Error> {
let name = SubscriberName::parse(value.name)?;
let email = SubscriberEmail::parse(value.email)?;
Ok(Self { email, name })
}
}
API接收了一个电子邮件地址和一个名字,作为用户提交的表单的附件数据。这两个字段都要经过另外的验证:SubscriberName::parse
和 SubscriberEmail::parse
。这两个方法是容易出错的,它们返回一个String作为错误类型来解释出错的原因。
//! src/domain/subscriber_email.rs
// [...]
impl SubscriberEmail {
pub fn parse(s: String) -> Result<SubscriberEmail, String> {
if validate_email(&s) {
Ok(Self(s))
} else {
Err(format!("{} is not a valid subscriber email.", s))
}
}
}
我必须承认,这不是最有用的错误信息:虽然我们告诉用户,他们输入的电子邮件地址是错误的,但我们没有帮助他们确定原因。 最后,这并不重要:我们没有把这些错误信息作为API响应的一部分发送给用户。而他们得到的是一个没有正文的 400 bad request
。
//! src/routes/subscription.rs
// [...]
pub async fn subscribe(/* */) -> HttpResponse {
let new_subscriber = match form.0.try_into() {
Ok(form) => form,
Err(_) => return HttpResponse::BadRequest().finish(),
};
// [...]
这是一个糟糕的错误:用户被蒙在鼓里,不能按返回的信息调整他们的提交行为(也不懂错在哪)。
总结
让我们总结一下到目前为止我们发现的问题。错误主要有两个作用:
- 控制流(即决定下一步做什么)
- 报告(充当事后调查的凭证,追溯什么地方出错了)
我们还可以根据错误的位置进行区分:
- 内部错误(即一个函数在我们的应用程序中调用另一个函数)
- 边缘错误(即一个我们未能满足预期的API请求)
控制流是程序化的,决定下一步做什么所需的所有信息必须能够被机器访问:
- 我们使用类型(例如枚举变体)、方法和字段来处理内部错误
- 我们依靠状态代码来处理边缘的错误
相反,错误报告主要由人类来消费。内容必须根据用户的情况进行调整:
- 开发者可以接触到系统的内部结构:他们应该被提供尽可能多的关于故障模式的背景。
- 用户在应用程序之外:应用程序提供必要的信息来调整他们的行为(例如,修复错误的输入)。
转载自:https://juejin.cn/post/7059771490673262605