「深挖Rust」错误处理 — 8

264 阅读3分钟

「这是我参与2022首次更文挑战的第 9 天,活动详情查看:2022首次更文挑战」。


错误类型不丰富

那我们的日志呢? 让我们再看看。

export RUST_LOG="sqlx=error,info"
export TEST_LOG=enabled
cargo t subscribe_fails_if_there_is_a_fatal_database_error | bunyan
...
 INFO: [HTTP REQUEST - END]
    exception.details="StoreTokenError(
            A database failure was encountered while trying to
            store a subscription token.

        Caused by:
            error returned from database: column 'subscription_token'
            of relation 'subscription_tokens' does not exist)"
    exception.message="Failed to create a new subscriber.",
    target=tracing_actix_web::root_span_builder,
    http.status_code=500

我们仍然在exception.details中得到了底层 StoreTokenError 很好的表示,但它显示我们现在正在使用 SubscribeError#[derive(Debug)] 实现。虽然没有损失任何信息。

而exception.message就不一样了。不管是什么故障模式,我们总是得到 Failed to create a new subscriber。这不是很明确。

让我们细化我们的Debug和Display实现。

//! src/routes/subscriptions.rs
// [...]

// Remember to delete `#[derive(Debug)]`!
impl std::fmt::Debug for SubscribeError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        error_chain_fmt(self, f)
    }
}

impl std::error::Error for SubscribeError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            // &str does not implement `Error` - we consider it the root cause
            SubscribeError::ValidationError(_) => None,
            SubscribeError::DatabaseError(e) => Some(e),
            SubscribeError::StoreTokenError(e) => Some(e),
            SubscribeError::SendEmailError(e) => Some(e),
        }
    }
}

impl std::fmt::Display for SubscribeError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            SubscribeError::ValidationError(e) => write!(f, "{}", e),
            // What should we do here?
            SubscribeError::DatabaseError(_) => write!(f, "???"),
            SubscribeError::StoreTokenError(_) => write!(
                f,
                "Failed to store the confirmation token for a new subscriber."
            ),
            SubscribeError::SendEmailError(_) => {
                write!(f, "Failed to send a confirmation email.")
            },
        }
    }
}

Debug很容易解决:我们为 SubscribeError 实现了 Error Trait,包括 source,我们可以再次使用我们之前为 StoreTokenError 写的辅助函数(error_chain_fmt)。

当涉及到 Display 时,我们有一个问题--同样的DatabaseError变体被用于在以下情况下遇到的错误:

  • 从池中获取一个新的Postgres连接。
  • 在subscribers表中插入一个订阅者。
  • 提交SQL事务。

当为 SubscribeError 实现 Display 时,我们没有办法区分这三种情况中的哪一种,也就是说底层的错误类型是不够的。让我们通过为每个操作使用不同的枚举变量来消除歧义:

//! src/routes/subscriptions.rs
// [...]

pub enum SubscribeError {
    // [...]
    // No more `DatabaseError`
    PoolError(sqlx::Error),
    InsertSubscriberError(sqlx::Error),
    TransactionCommitError(sqlx::Error),
}

impl std::error::Error for SubscribeError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            //  [...]
            // No more DatabaseError
            SubscribeError::PoolError(e) => Some(e),
            SubscribeError::InsertSubscriberError(e) => Some(e),
            SubscribeError::TransactionCommitError(e) => Some(e),
            // [...]
        }
    }
}

impl std::fmt::Display for SubscribeError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            // [...]
            SubscribeError::PoolError(_) => {
                write!(f, "Failed to acquire a Postgres connection from the pool")
            }
            SubscribeError::InsertSubscriberError(_) => {
                write!(f, "Failed to insert new subscriber in the database.")
            }
            SubscribeError::TransactionCommitError(_) => {
                write!(
                    f,
                    "Failed to commit SQL transaction to store a new subscriber."
                )
            }
        }
    }
}

impl ResponseError for SubscribeError {
    fn status_code(&self) -> StatusCode {
        match self {
            SubscribeError::ValidationError(_) => StatusCode::BAD_REQUEST,
            SubscribeError::PoolError(_)
            | SubscribeError::TransactionCommitError(_)
            | SubscribeError::InsertSubscriberError(_)
            | SubscribeError::StoreTokenError(_)
            | SubscribeError::SendEmailError(_) => StatusCode::INTERNAL_SERVER_ERROR,
        }
    }
}

DatabaseError 在另一个地方使用:

//! src/routes/subscriptions.rs
// [..]

impl From<sqlx::Error> for SubscribeError {
    fn from(e: sqlx::Error) -> Self {
        Self::DatabaseError(e)
    }
}

仅仅是类型还不足以区分应该使用哪种枚举变量;我们不能为 sqlx::Error 实现 From。我们必须使用 map_err 来在每种情况下进行正确的转换。

//! src/routes/subscriptions.rs
// [..]

pub async fn subscribe(/* */) -> Result<HttpResponse, SubscribeError> {
    // [...]
    let mut transaction = pool.begin().await.map_err(SubscribeError::PoolError)?;
    let subscriber_id = insert_subscriber(&mut transaction, &new_subscriber)
        .await
        .map_err(SubscribeError::InsertSubscriberError)?;
    // [...]
    transaction
        .commit()
        .await
        .map_err(SubscribeError::TransactionCommitError)?;
    // [...]
}

代码编译后,exception.message 再次显示有用的信息:

...
 INFO: [HTTP REQUEST - END]
    exception.details="Failed to store the confirmation token
        for a new subscriber.

        Caused by:
            A database failure was encountered while trying to store
            a subscription token.
        Caused by:
            error returned from database: column 'subscription_token'
            of relation 'subscription_tokens' does not exist"
    exception.message="Failed to store the confirmation token for a new subscriber.",
    target=tracing_actix_web::root_span_builder,
    http.status_code=500