Rustのエラーライブラリ比較（failure/err_derive/snafu）

# はじめに
Rustのエラーライブラリは長らくfailureがデファクトスタンダードでしたが、最近は新しいクレートがいくつか出てきています。
今回は特に「CLIツールでいい感じにエラーメッセージを出す」という観点で、3つのクレートを比較してみました。

# 比較するクレート

["Rustのエラーライブラリ利用状況を調べてみた"](https://qiita.com/dalance/items/4704b16c0718f6dfb7c0)の結果を元に以下の3つを選びました。デファクトスタンダードであるfailureに対して、後発のクレートはどうか、という比較になります。

* failure
* snafu
* err_derive

# サンプルアプリ
何らかのファイルを読むアプリを想定します。すなわちこんな感じです。

```rust
let path = "test";
let mut f = File::open(&path).unwrap();
let mut s = String::new();
let _ = f.read_to_string(&mut s);
dbg!(s);
```

もしtestファイルが存在しなければ`unwrap`している部分で`std::io::Error`が発生するので、それを適切に表示したい、ということになります。ここでは以下のような表示を目指します。

```
Failed to open file ("test")
  Caused by : No such file or directory (os error 2)
```

また、各クレートが推奨する方法の範囲内でなるべくシンプルに書く方針です。

## failure

failureはいくつかの使い方がありますが、CLIツールで使うなら["Use the Error type"](https://boats.gitlab.io/failure/use-error.html)というパターンが簡単です。

```rust
use failure::{Error, ResultExt};
use std::{fs::File, io::Read};

fn main() {
    if let Err(e) = run() {
        eprintln!("{}", e);
        for e in e.as_fail().iter_causes() {
            eprintln!("  Caused by: {}", e);
        }
    }
}

fn run() -> Result<(), Error> {
    let path = "test";
    let mut f = File::open(&path).context(format!("Failed to open file ({:?})", path))?;
    let mut s = String::new();
    let _ = f.read_to_string(&mut s);
    dbg!(s);
    Ok(())
}
```

エラー型は`failure::Error`で、`context`メソッドでエラーコンテキストを追加できます。
また`as_fail`で得られる`Fail`トレイトは`iter_causes`を提供するので、"Caused by"の部分はループで書くことができます。

## err_derive

```rust
use err_derive::Error;
use std::error::Error as StdError;
use std::{fs::File, io::Read, path::PathBuf};

#[derive(Debug, Error)]
enum Error {
    #[error(display = "Failed to open file ({:?})", path)]
    OpenFile {
        #[error(cause)]
        source: std::io::Error,
        path: PathBuf,
    },
}

fn main() {
    if let Err(e) = run() {
        eprintln!("{}", e);
        let mut cause = e.source();
        while let Some(e) = cause {
            eprintln!("  Caused by: {}", e);
            cause = e.source();
        }
    }
}

fn run() -> Result<(), Error> {
    let path = "test";
    let mut f = File::open(&path).map_err(|x| Error::OpenFile {
        source: x,
        path: path.into(),
    })?;
    let mut s = String::new();
    let _ = f.read_to_string(&mut s);
    dbg!(s);
    Ok(())
}
```

err_deriveはstd::error::Errorトレイトを実装するエラー型をderiveで自動導出するタイプです。
`use std::error::Error`とすると`Error`という名前が使えなくなってしまうので`StdError`としてインポートしています。
ユーザが定義したエラー型に`#[derive(Error)]`を付けると必要なimplが生成されます。
また、エラーの原因を示すフィールドは`#[error(cause)]`を付けておくと、あとで`source`メソッドで辿ることができます。
エラーコンテキストの追加は`context`のような簡便な方法はなく、`map_err`するようです。
また、原因を辿る部分もイテレータがないので`while`で辿る必要があります。

std::error::Errorを使っているので将来性はありそうな気もするのですが、現時点ではいろいろと辛い感じがします。

## snafu

```rust
use snafu::{ResultExt, Snafu};
use std::{fs::File, io::Read, path::PathBuf};

#[derive(Debug, Snafu)]
enum Error {
    #[snafu(display("Failed to open file ({:?})\n  Caused by : {}", path, source))]
    OpenFile {
        source: std::io::Error,
        path: PathBuf,
    },
}

fn main() {
    if let Err(e) = run() {
        eprintln!("{}", e);
    }
}

fn run() -> Result<(), Error> {
    let path = "test";
    let mut f = File::open(&path).context(OpenFile { path })?;
    let mut s = String::new();
    let _ = f.read_to_string(&mut s);
    dbg!(s);
    Ok(())
}
```

エラー型はユーザが定義して`#[derive(Snafu)]`とします。snafuはfailureやstd::error::Errorと違う独自路線を行っているようで、エラーの原因を辿るための`cause`や`source`といったメソッドは用意されていません。
その代わり`source`というフィールドに原因となったエラーを入れることができます。
`context`メソッドは元のエラーをこの`source`に勝手に入れてくれるので、`context`部分は非常にシンプルです。

見た目は結構いい感じですが、独自路線というところは気になります。