🌟

Rustのエラー処理(Result型)を色々試して、なんとなく良さそうな形が分かった

2024/03/24に公開

はじめに

Rustを始めて間もなく、エラーの型やResultで詰まりました。std::error::Errorstd::io::ErrorResultanyhow::Result。どれを使ったらいいのか。。。

そこで、基本的なものから独自のエラー型まで一通り試したので紹介します。

1. 標準ライブラリを使う

最もベーシックな方法で、stdライブラリのみを使用します。
fooから返ったエラーにbarのエラーメッセージを追加してprintする単純な処理です。

main.rs
use std::error::Error;
use std::io;

fn foo() -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
    Err(Box::new(io::Error::new(io::ErrorKind::Other, "fooのエラー")))
}

fn bar() -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
    match foo() {
        Err(err) => Err(Box::new(io::Error::new(
            io::ErrorKind::Other,
            format!("barのエラー: {}", err),
        ))),
        Ok(_) => Ok(()),
    }
}

fn main() {
    if let Err(e) = bar() {
        println!("{:#}", e); // barのエラー: fooのエラー
    }
}

戻り値の型はResult<(), Box<dyn Error + Send + Sync + 'static>>でちょっと長いですね。
これは、Errorでは任意のエラーを示し、Send + Sync + 'staticでスレッド間での受け渡しを可能にしています。

ただし、全てのエラーを返すことができる上に、呼び出し側は全てのエラーに対応しなければならず、ハンドリングが複雑です。それにResultが長くて毎回書くのは大変......

この処理を多くの関数に書くのはちょっと面倒ですよね。
ということで、型エイリアスを使って短く書きましょう。

2. 標準ライブラリを使う(型エイリアスを使う)

先ほどとロジックは変わりませんが、ネックだったResult<(), Box<dyn Error + Send + Sync + 'static>>を短く書きます。

GenericErrorGenericResult<T>という独自型(エイリアス)を定義して記述量を減らせるので便利です。

main.rs
use std::error::Error;
use std::io;

type GenericError = Box<dyn Error + Send + Sync + 'static>;
type GenericResult<T> = Result<T, GenericError>;

fn foo() -> GenericResult<()> {
    Err(Box::new(io::Error::new(io::ErrorKind::Other, "fooのエラー")))
}

fn bar() -> GenericResult<()> {
    match foo() {
        Err(err) => Err(Box::new(io::Error::new(
            io::ErrorKind::Other,
            format!("barのエラー: {}", err),
        ))),
        Ok(_) => Ok(()),
    }
}

fn main() {
    if let Err(e) = bar() {
        println!("{:#}", e); // barのエラー: fooのエラー
    }
}

こちらのコードは以下の書籍のサンプルコードを使用しています。
https://www.oreilly.co.jp/books/9784873119786/

記述量が減ったとしても、バックトレースを自分で実装する必要があるなど、便利機能は備わっていません。
そこで、外部のクレートを使ってみます。

3. anyhowクレートを使う

エラー処理で多く使われているanyhowクレートを使って書いてみます。

https://docs.rs/anyhow/latest/anyhow/

anyhowについてはドキュメントや記事最後の参考記事を見てください。

エラーがとてもシンプルに書けるようになりました。
ただし、このコードだとエラーには文字列しか入っていないため、エラーの種類を定義したくなります。

use anyhow::{anyhow, Context, Result};

fn foo() -> Result<()> {
    Err(anyhow!("fooのエラー"))
}

fn bar() -> Result<()> {
    foo().context("barのエラー")
}

fn main() {
    if let Err(e) = bar() {
        // println!("{:?}", e); バックトレース表示(RUST_BACKTRACE=1)

        println!("{:#}", e); // barのエラー: fooのエラー
    }
}

次は独自のエラー型を定義してみます。

4. 独自のエラー型を定義する

enum MyErrorを作成し、ErrorKind1でエラーの種類を定義しました。
このバリアントを追加していけば、エラーの種類を増やすことができます。

use std::fmt::{Display, Formatter, Result as FmtResult};

#[derive(Debug)]
enum MyError {
    ErrorKind1(String),
}

impl Display for MyError {
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        match self {
            MyError::ErrorKind1(msg) => write!(f, "[Kind1]{}", msg),
        }
    }
}

fn foo() -> Result<(), MyError> {
    Err(MyError::ErrorKind1("fooのエラー".to_string()))
}

fn bar() -> Result<(), MyError> {
    foo().map_err(|err| MyError::ErrorKind1(format!("barのエラー: {}", err)))
}

fn main() {
    if let Err(e) = bar() {
        println!("{}", e); // barのエラー: [Kind1]fooのエラー
    }
}

エラーをカスタムできるので、やりたいことは基本的にこれで叶いそうです。
ただ、thiserrorクレートでDisplayをもっと簡単に記述できるので、使ってみましょう。

5. 独自のエラー型 + thiserrorを使う

先ほどの独自エラー型のDisplayの記述をthiserrorを使うことで簡単にしてみましょう。

https://docs.rs/thiserror/latest/thiserror/

だいぶスッキリしました。

#[derive(Debug, thiserror::Error)]
enum MyError {
    #[error("[Kind1]{0}")]
    ErrorKind1(String),
}

fn foo() -> Result<(), MyError> {
    Err(MyError::ErrorKind1("fooのエラー".to_string()))
}

fn bar() -> Result<(), MyError> {
    foo().map_err(|err| MyError::ErrorKind1(format!("barのエラー: {}", err)))
}

fn main() {
    if let Err(e) = bar() {
        println!("{}", e); // barのエラー: [Kind1]fooのエラー
    }
}

ただ、anyhowは.context()やバックトレースなど便利機能が備わってるので使いたい.....
次は今までの便利機能をまとめて使ってみましょう。

6. anyhow + 独自のエラー型 + thiserror

個人的には、今のところこれが一番感じだと思っています。
もっといい方法があれば教えてください。
(独自のエラー型(struct)を作って、その中にanyhowのエラーをフィールドとして入れるのもいいと思ってます)

.context()で1つ前のエラーにメッセージを追加でき、バックトレースも簡単に表示できます。
独自型だけで定義すると型を矯正できるのでメリットは大きいですが、その分記述量も多くなります。

このコードはanyhowのメリットを享受しつつ、独自の型を使えるのはメリットだと思います。

use anyhow::{anyhow, Context, Result as AnyhowResult};
use thiserror::Error;
use MyError::ErrorKind1;

#[derive(Debug, Error)]
enum MyError {
    #[error("[kind1]{0}")]
    ErrorKind1(String),
}

fn foo() -> AnyhowResult<()> {
    Err(anyhow!(ErrorKind1("fooのエラー".to_string())))
}

fn bar() -> AnyhowResult<()> {
    foo().context("barのエラー")
}

fn main() {
    if let Err(e) = bar() {
        println!("{:#}", e); // barのエラー: [kind1]fooのエラー
    }
}

さいごに

まだまだ改善の余地はあると思うので、引き続きベストな方法を探っていきます。

参考記事

以下の記事を参考にさせていただきました。ありがとうございます。
https://zenn.dev/shimopino/articles/understand-rust-error-handling#anyhow-クレート
https://zenn.dev/yukinarit/articles/b39cd42820f29e

Discussion