rs.nkmk.me

Rust, anyhowによる基本的なエラー処理

Posted: | Tags: Rust, anyhow, エラー処理

Rustのanyhowを使うとエラー処理が簡単にできるようになる。

ここではanyhowの基本的な使い方について説明する。

本記事のサンプルコードにおけるanyhowのバージョンは以下の通り。

anyhow = "1.0"
source: Cargo.toml

また、use宣言は以下の通り。

use anyhow::{ensure, Context, Result};
use std::fs;

Result<T, anyhow::Error>で複数のエラー型に対応

anyhow::Errorはエラー型のラッパー。あらゆるエラー型からanyhow::Errorに変換できるようになっている。

関数の返り値の型をResult<T, anyhow::Error>とすると、?演算子で異なる複数の型のエラーを呼び出し元に委譲できる。

さらにcontextwith_contextを使うと、Noneanyhow::Errorとして委譲することも可能(後述)。

なお、anyhow::Result<T>Result<T, anyhow::Error>の型エイリアスとして定義されているので、use anyhow::Result;とすると、Result<T, anyhow::Error>Result<T>と書ける。

型エイリアスについては以下の記事を参照。

具体例

std::fs::read_to_stringでテキストファイルを読み込み、parseで数値に変換して何らかの処理を行う関数を例とする。ここでは単に10倍にして返す。

read_to_stringおよびparseはそれぞれ異なる型のエラーを返す。

前準備として、中身が123のファイルとabcのファイルを生成する。それぞれのパスと存在しないパスの文字列も定義しておく。

fs::create_dir_all("data/temp").unwrap();

let path_123 = "data/temp/123.txt";
fs::write(path_123, "123").unwrap();

let path_abc = "data/temp/abc.txt";
fs::write(path_abc, "abc").unwrap();

let path_non_existent = "path/to/non_existent.txt";

unwrapを使うと以下のようになる。中身が数値に変換できない文字列だったり、存在しないパスを指定したりすると、実行時にパニックになってしまう。

fn with_unwrap(path: &str) -> i32 {
    let s = fs::read_to_string(path).unwrap();
    let i = s.parse::<i32>().unwrap();

    i * 10
}

assert_eq!(with_unwrap(path_123), 1230);

// let x = with_unwrap(path_abc);
// thread 'main' panicked at 'called `Result::unwrap()` ...

// let x = with_unwrap(path_non_existent);
// thread 'main' panicked at 'called `Result::unwrap()` ...

anyhowを使う例は以下の通り。?演算子でエラーを委譲する。

fn with_anyhow(path: &str) -> Result<i32> {
    let s = fs::read_to_string(path)?;
    let i = s.parse::<i32>()?;

    Ok(i * 10)
}

assert_eq!(with_anyhow(path_123).unwrap(), 1230);

println!("{:?}", with_anyhow(path_abc).unwrap_err());
// invalid digit found in string

println!("{:?}", with_anyhow(path_non_existent).unwrap_err());
// No such file or directory (os error 2)

ここでは、簡単のため、返り値に対してunwrapunwrap_errで値やエラーを取り出して確認しているが、実際のプログラムでは適切なエラー処理を行えばよい。以降の例も同じ。

anyhow::Contextでコンテキストを追加

上の例のように、デフォルトのエラーメッセージには問題の発生箇所などの情報が含まれていない。

anyhow::Contextトレイトをインポートとすると、ResultおよびOptioncontext, with_contextメソッドが使えるようになる。

Contextという名前を使うわけではないので、use anyhow::Context as _;としてインポートすることも可能。

with_contextでコンテキストを追加する例。問題の詳細をエラーメッセージに追加できる。

fn with_anyhow_context(path: &str) -> Result<i32> {
    let s = fs::read_to_string(path).with_context(|| format!("Failed to read {path:?}"))?;
    let i = s
        .parse::<i32>()
        .with_context(|| format!("Failed to parse {s:?}"))?;

    Ok(i * 10)
}

assert_eq!(with_anyhow_context(path_123).unwrap(), 1230);

println!("{:?}", with_anyhow_context(path_abc).unwrap_err());
// Failed to parse "abc"
//
// Caused by:
//     invalid digit found in string

println!("{:?}", with_anyhow_context(path_non_existent).unwrap_err());
// Failed to read "path/to/non_existent.txt"
//
// Caused by:
//     No such file or directory (os error 2)

contextとwith_context

contextは先行評価で、エラーかどうかに関わらず指定された引数が評価される。一方、with_contextは遅延評価で、エラーのときのみ指定されたクロージャが実行される。

上の例ではwith_context(|| format!(...))としている。|| format!(...)は引数なしのクロージャ。この場合、エラーのときのみformat!が実行される。

context(format!(...))とすることも可能だが、この場合、エラーかどうかに関わらずformat!が実行されてしまい、余計な処理が発生する。

固定値の場合はcontextでもいいが、format!やそのほかの関数などの結果をコンテキストとして追加したい場合はwith_contextを使うほうが望ましい。

*_or*_or_elseの違いと同じ。

OptionをResultに変換

contextおよびwith_contextResultだけでなくOptionからも呼べる。Noneの場合にanyhow::Errorreturnする。

以下の例では最後にchecked_add100を足している。checked_addはオーバーフローしたときにNoneを返す。

ResultのエラーもOptionNoneも呼び出し元に委譲できる。

fn with_anyhow_option(path: &str) -> Result<i32> {
    let s = fs::read_to_string(path).with_context(|| format!("Failed to read {path:?}"))?;
    let i = s
        .parse::<i32>()
        .with_context(|| format!("Failed to parse {s:?}"))?;
    let i = i.checked_add(100).context("Overflow occurred")?;

    Ok(i)
}

let path_max = "data/temp/max.txt";
fs::write(path_max, i32::MAX.to_string()).unwrap();

println!("{:?}", with_anyhow_option(path_max).unwrap_err());
// Overflow occurred

assert_eq!(with_anyhow_option(path_123).unwrap(), 223);
assert!(with_anyhow_option(path_abc).is_err());
assert!(with_anyhow_option(path_non_existent).is_err());

read_to_stringparseのエラーについては上の例と同じなのでis_errでエラーであることの確認のみをしている。

その他の例は以下の記事を参照。

マクロ(anyhow!, bail!, ensure!)で単発のエラーを返す

anyhowは単発のエラーを返すのに便利なマクロも提供している。

以下の例では、ensure!マクロを使って最終的な値が1000未満の場合にエラーを返している。

fn with_anyhow_macro(path: &str) -> Result<i32> {
    let s = fs::read_to_string(path).with_context(|| format!("Failed to read {path:?}"))?;
    let i = s
        .parse::<i32>()
        .with_context(|| format!("Failed to parse {s:?}"))?;
    let i = i.checked_add(100).context("Overflow occurred")?;

    ensure!(i >= 1000, "Value must be at least 1000, got {}", i);

    Ok(i)
}

println!("{:?}", with_anyhow_macro(path_123).unwrap_err());
// Value must be at least 1000, got 223

assert!(with_anyhow_option(path_abc).is_err());
assert!(with_anyhow_option(path_non_existent).is_err());
assert!(with_anyhow_option(path_max).is_err());

anyhowのマクロについての詳細は以下の記事を参照。

関連カテゴリー

関連記事