エラーハ理

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という2つの主要なクレートがよく使用されます:

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

  • 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フィーチャーを有効にする必要があります:

[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))
}

エラーにはしばしば機密情報が含まれており、通常は一般ユーザーに見せたくないものです。しかし、開発者やサイト管理者であれば、エラーがすべての詳細を赤裸々に表示してくれることを望むかもしれません。

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に組み込まれているエラーページはほとんどの場合に十分ですが、場合によってはエラーページの表示をカスタマイズしたいこともあります。

これはカスタムCatcherを実装することで可能です。詳細についてはCatcherのセクションを参照してください。