Rustのエラーハンドリングを楽にするanyhowの使い方

Rustでいろいろなエラー型のエラー処理が大変だなと感じる場合は、dtolnay/anyhow を使うことで簡単にエラーハンドリングをシンプルにすることができます。

この記事では、anyhowの主要な使い方を3つ紹介しながら、どれだけ簡単にエラー処理を実装できるかを説明していきます。ちなみに、詳細までは説明しないので、知りたい方はREADMEをご覧ください。

anyhowの概要

はじめにanyhowの特徴について簡単に説明します。

  • std::result::Result<T, E>型を使いやすくしたanyhow::Result<T>型が便利
  • context()with_context()でエラーに簡単にコンテキスト情報を付与できる
  • anyhow!bail!マクロでanyhow::Result<T>型のエラーを簡単に作成できる

anyhow::Result型が便利

anyhow::Resultは、次のようにstd::result::ResultEを省略可能な形で定義されています。

pub type Result<T, E = Error> = Result<T, E>;

そのため、以下のようにanyhow::Resultを使うことでResultを簡単に記載することができるようになります。

// anyhow::Result を使わない場合
fn maybe_failure() -> Result<(), SomethingError> { ... }


// anyhow::Result を使う場合
use anyhow::Result;

fn maybe_failure() -> Result<()> { ... }
// ^ fn maybe_failure() -> std::result::Result<T, anyhow::Error> { ... } と同様

// anyhow::Result でErrorの型を指定することも可能
fn maybe_failure() -> Result<(), SomethingError> { ... }
// ^ fn maybe_failure() -> std::result::Result<T, SomethingError> { ... } と同様

また、anyhow::ErrorBox<dyn std::error::Error>のように振る舞います。そのため、以下のようにさまざまなエラータイプを返す関数において、?を使って見通しがよいコードが書けます。

use anyhow::Result;
use serde_json::Value;

fn main() -> Result<()> {
    // ファイルが見つからない場合 std::io::Error を返す
    let config = std::fs::read_to_string("cluster.json")?;
    // ファイル内の文字列をJSONにパースできない場合 serde_json::Error を返す
    let map: Value = serde_json::from_str(&config)?;
    println!("cluster info: {:#?}", map);
    Ok(())
}

context()やwith_context()でエラーにコンテキスト情報を付与

context()with_context()を使うことでエラーにコンテキスト情報を付与できます。

それぞれの違いは、contextはエラーにコンテキスト情報を付与、with_context()はエラー発生時のみ遅延評価されコンテキスト情報を付与します。

use anyhow::{Context, Result};
use std::fs;
use std::path::PathBuf;

pub struct ImportantThing {
    path: PathBuf,
}

pub fn do_it(it: ImportantThing) -> Result<Vec<u8>> {
    let path = &it.path;
    # with_context()でエラー時にコンテキスト情報を付与する
    let content = fs::read(path)
        .with_context(|| format!("Failed to read instrs from {}", path.display()))?;

    Ok(content)
}

fn main() -> Result<()> {
    let it = ImportantThing {
        path: PathBuf::from("instructions.txt"),
    };
    let content = do_it(it)?;
    println!("{:?}", content);
    Ok(())
}

上記のコードを実行すると以下のようなコンテキスト付きのエラーメッセージが表示されます。

Error: Failed to read instrs from instructions.txt

Caused by:
    No such file or directory (os error 2)

anyhow!やbail!マクロでanyhow::Result<T>型のエラーを作成

anyhow::Result型を簡単に作成するanyhow!bail!マクロがあります。

use anyhow::{anyhow, Result};

fn validate(key: &str) -> Result<()> {
    if key.len() != 16 {
        return Err(anyhow!("key length must be 16 characters, got {:?}", key));
    }

    Ok(())
}

fn main() {
    let key = "1234567890";
    let err = validate(key).unwrap_err();
    println!("Validation Error: {}", err);
    // Validation Error: key length must be 16 characters, got "1234567890"
}

また、bail!を使うとreturn Err(anyhow!(...)と書く必要がなります。

use anyhow::{bail, Result};

fn validate(key: &str) -> Result<()> {
    if key.len() != 16 {
        bail!("key length must be 16 characters, got {:?}", key);
    }

    Ok(())
}

fn main() {
    let key = "1234567890";
    let err = validate(key).unwrap_err();
    println!("Validation Error: {}", err);
    // Validation Error: key length must be 16 characters, got "1234567890"
}

参考