エラー処理

Rust アプリケーションにおける一般的なエラー処理手法

Rustのエラー処理はJavaなどの言語とは異なり、try...catchのような仕組みは存在しません。一般的な手法は、アプリケーションレベルでグローバルなエラー処理型を定義することです:

use thiserror::Error;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("io: `{0}`")]
    Io(#[from] io::Error),
    #[error("utf8: `{0}`")]
    FromUtf8(#[from] FromUtf8Error),
    #[error("diesel: `{0}`")]
    Diesel(#[from] diesel::result::Error),
    ...
}

pub type AppResult<T> = Result<T, AppError>;

ここではthiserrorライブラリを使用しており、独自のカスタムエラー型を簡単に定義し、コードを簡潔に記述できます。記述を簡略化するために、AppResultも併せて定義しています。

thiserror と anyhow

Rustのエラー処理エコシステムでは、thiserroranyhowがよく使用されるライブラリです:

  • thiserror: ライブラリ開発者向けで、明確なエラー型を定義するために使用されます。派生マクロを通じてカスタムエラー型にstd::error::Errorトレイトを実装するのを支援し、エラーの表現方法を定義できます。ライブラリを構築する場合や、ユーザーに明確なエラー型を提供する必要がある場合、thiserrorがより適しています。

  • anyhow: アプリケーション開発者向けで、汎用的なエラー型anyhow::Errorを提供します。これはstd::error::Errorトレイトを実装したあらゆるエラーを含むことができます。エラーの定義よりも伝播に重点を置いており、特にアプリケーション層のコードに適しています。様々なエラーをanyhow::Errorに迅速に変換でき、ボイラープレートコードの記述を減らすことができます。

一部のシナリオでは、これら2つのライブラリを併用することがあります:ライブラリではthiserrorでエラー型を定義し、アプリケーションではanyhowでこれらのエラーを処理・伝播します。

Handlerにおけるエラー処理

Salvoでは、Handlerも様々なエラーに遭遇することがよくあります。例えば:データベース接続エラー、ファイルアクセスエラー、ネットワーク接続エラーなどです。この種のエラーに対しては、前述のエラー処理手法を採用できます:

#[handler]
async fn home()-> AppResult<()> {

}

ここでのhomeは直接AppResult<()>を返しています。しかし、このエラーはどのように表示されるべきでしょうか?AppResultというカスタムエラー型にWriterを実装する必要があり、この実装の中でエラーの表示方法を決定できます:

#[async_trait]
impl Writer for AppError {
    async fn write(mut self, _req: &mut Request, depot: &mut Depot, res: &mut Response) {
        res.render(Text::Plain("I'm a error, hahaha!"));
    }
}

SalvoのHandlerResultを返すことができ、Result内のOkErrの型が両方ともWriterトレイトを実装している必要があります。

anyhowを使用したエラー処理

anyhowの使用が比較的広まっていることを考慮し、Salvoはanyhow::Errorに対する組み込みサポートを提供しています。anyhow機能を有効にすると、anyhow::ErrorWriterトレイトを実装し、InternalServerErrorにマッピングされます:

#[cfg(feature = "anyhow")]
#[async_trait]
impl Writer for ::anyhow::Error {
    async fn write(mut self, _req: &mut Request, _depot: &mut Depot, res: &mut Response) {
        res.render(StatusError::internal_server_error());
    }
}

anyhow機能を使用するには、Cargo.tomlでSalvoのanyhow featureを有効にする必要があります:

[dependencies]
salvo = { version = "*", features = ["anyhow"] }
anyhow = "1.0"

これにより、ハンドラ関数は直接anyhow::Result<T>を返せるようになります:

#[handler]
async fn home() -> anyhow::Result<impl Writer> {
    let data = fetch_data().context("Failed to fetch data")?;
    Ok(Text::Plain(data))
}

Errorにはしばしば機密情報が含まれており、一般的には一般ユーザーに見せたくないものです。そうでなければあまりにも安全ではなく、プライバシーも少しもなくなってしまいます。しかし、開発者やサイト管理者であれば、考え方は異なるかもしれません。エラーがすべてをさらけ出し、最も真実のエラー情報を見せてほしいと望むでしょう。

writeメソッドでは、実際にRequestDepotの参照を取得できることがわかります。これにより、上記のような巧妙な操作を簡単に実装できます:

#[async_trait]
impl Writer for AppError {
    async fn write(mut self, _req: &mut Request, depot: &mut Depot, res: &mut Response) {
        let user = depot.obtain::<User>();
        if user.is_admin {
            res.render(Text::Plain(e.to_string()));
        } else {
            res.render(Text::Plain("I'm a error, hahaha!"));
        }
    }
}

エラーページの表示

Salvoに組み込まれているエラーページは、ほとんどの場合で要件を満たしています。リクエストのデータタイプに応じてHtml、Json、またはXmlページを表示できます。しかし、一部の場合では、依然としてエラーページの表示をカスタマイズしたいと考えるでしょう。

これはカスタムCatcherを実装することで実現できます。詳細な説明はCatcherのセクションを参照してください。