Rustのエラー処理(Result型)を色々試して、なんとなく良さそうな形が分かった
はじめに
Rustを始めて間もなく、エラーの型やResultで詰まりました。std::error::Error
やstd::io::Error
、Result
にanyhow::Result
。どれを使ったらいいのか。。。
そこで、基本的なものから独自のエラー型まで一通り試したので紹介します。
1. 標準ライブラリを使う
最もベーシックな方法で、std
ライブラリのみを使用します。
foo
から返ったエラーにbar
のエラーメッセージを追加してprintする単純な処理です。
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>>
を短く書きます。
GenericError
とGenericResult<T>
という独自型(エイリアス)を定義して記述量を減らせるので便利です。
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のエラー
}
}
こちらのコードは以下の書籍のサンプルコードを使用しています。
記述量が減ったとしても、バックトレースを自分で実装する必要があるなど、便利機能は備わっていません。
そこで、外部のクレートを使ってみます。
3. anyhowクレートを使う
エラー処理で多く使われている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
を使うことで簡単にしてみましょう。
だいぶスッキリしました。
#[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のエラー
}
}
さいごに
まだまだ改善の余地はあると思うので、引き続きベストな方法を探っていきます。
参考記事
以下の記事を参考にさせていただきました。ありがとうございます。
Discussion