TL;DR
std::error::Errorトレイトは実装が簡単なので、エラー型を定義するなら実装する。- 元になるエラーが複数あるとき、その型を保持する必要があるかを考える。必要なら、アプリケーションでは anyhow で包む。必要ないなら
enumで表し、thiserror で変換を定義する。 - いずれにせよ型システムで宣言する(項目1)。
内容
https://effective-rust.com/errors.html
Error トレイト
Error はエラーの共通インタフェースとなる標準のトレイトである。実装するには Display({} 用)と Debug({:?} 用)も実装する必要がある。メソッドは、非推奨のものを除けば、内部に持つエラーを返す source だけである。source には None を返すデフォルト実装があるので、実装は簡単である。
Result<T, E> の E は Error を実装していなくてもよいが、ラッパが適切なトレイト境界を書けるように、エラー型には Error を実装しておく。なお、Error は 1.81 で std から core に移ったので、no_std の環境でも使える(項目33)。
最小限のエラー
情報を持つ必要がなければ、エラーは String でよい。Stringly typed が適切な数少ない例である。しかし String は Error を実装していない。impl Error for String は、トレイトも型も自分のクレートのものではないので孤児ルールで書けない。型エイリアスも新しい型を作らないので同じである。
そこで Newtype パターンで String を包み、Debug、Display、Error を実装する。
#[derive(Debug)]
pub struct MyError(String);
impl std::fmt::Display for MyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for MyError {}
impl From<String> for MyError {
fn from(value: String) -> Self {
Self(value)
}
}From<String> を実装しておくと、? が自動で From の変換を適用する。
ネストしたエラー
内部のエラーの詳細を呼び出し元に伝えるには、enum を使う。
#[derive(Debug)]
pub enum MyError {
Io(std::io::Error),
Utf8(std::string::FromUtf8Error),
General(String),
}内部のエラーにアクセスできるよう source をオーバーライドし、各エラー型からの From も実装する。こうすると、ライブラリの利用者は孤児ルールに悩まされず、? が変換するので map_err も要らない。ただし、安全なエラー型を定義するには定型的なコードが多くなる。derive マクロで避けられるが、自作するより thiserror クレートを使うほうがよい。
トレイトオブジェクト
文字列だけを持つ方法はエラーの詳細を捨ててしまう。enum の方法は情報をすべて持てるが、起こりうるエラーをすべて列挙する必要がある。Box<dyn Error> のようなトレイトオブジェクトを使えば列挙は要らないが、型の情報は消える。Error とそのトレイト境界のメソッドは呼べても、元のエラー型のメソッドは呼べない。
さらに、トレイトオブジェクト安全性や、1 つの型に対するトレイトの実装は 1 つだけという一貫性ルール(coherence rule)のために、Error と From の両方を実装するのが難しい。次のコードは、標準ライブラリの impl<T> From<T> for T というブランケット実装と衝突し、適用する実装を 1 つに絞れないので、コンパイルエラーになる。
impl Error for WrappedError {}
impl<E: 'static + Error> From<E> for WrappedError {
fn from(e: E) -> Self {
Self::Wrapped(Box::new(e))
}
}anyhow とライブラリの例外
anyhow は、Box による間接参照の層を加えてこの問題を解決している。スタックトレースなどの機能もある。
アプリケーションのエラー処理には anyhow の利用を検討しよう
ただし、ライブラリは例外である(参考: https://nick.groenen.me/posts/rust-error-handling/ )。ライブラリは使われる環境を予想できないので、利用者が使える具体的なエラー情報を返すべきである。そのため enum のエラーを使い、API で anyhow に依存するのは避ける(項目24)。一方、アプリケーションはエラーをエンドユーザにどう見せるかに注力すべきで、依存関係にあるすべてのライブラリのエラーを扱う必要がある(項目25)。anyhow::Error などで動的に扱うほうが単純になり、アプリケーション全体で一貫する。
キーワード
自分の理解
From の実装が衝突する例をより具体的にすると、次のようになる。
let error: WrappedError = WrappedError::General("hoge".into());
let wrapped: WrappedError = error.into();このとき、コンパイラは 2 つの候補を検討する。1 つは標準ライブラリの impl<T> From<T> for T で、T = WrappedError として WrappedError をそのまま WrappedError に変換する。もう 1 つは自作の実装で、E = WrappedError と解釈できてしまう。