Rust, anyhowによる基本的なエラー処理
Rustのanyhowを使うとエラー処理が簡単にできるようになる。
ここではanyhowの基本的な使い方について説明する。
本記事のサンプルコードにおけるanyhowのバージョンは以下の通り。
anyhow = "1.0"
また、use
宣言は以下の通り。
use anyhow::{ensure, Context, Result};
use std::fs;
Result<T, anyhow::Error>で複数のエラー型に対応
anyhow::Error
はエラー型のラッパー。あらゆるエラー型からanyhow::Error
に変換できるようになっている。
関数の返り値の型をResult<T, anyhow::Error>
とすると、?
演算子で異なる複数の型のエラーを呼び出し元に委譲できる。
さらにcontext
やwith_context
を使うと、None
をanyhow::Error
として委譲することも可能(後述)。
なお、anyhow::Result<T>
がResult<T, anyhow::Error>
の型エイリアスとして定義されているので、use anyhow::Result;
とすると、Result<T, anyhow::Error>
をResult<T>
と書ける。
型エイリアスについては以下の記事を参照。
- 関連記事: Rustのtypeで型エイリアスを定義
具体例
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)
ここでは、簡単のため、返り値に対してunwrap
やunwrap_err
で値やエラーを取り出して確認しているが、実際のプログラムでは適切なエラー処理を行えばよい。以降の例も同じ。
anyhow::Contextでコンテキストを追加
上の例のように、デフォルトのエラーメッセージには問題の発生箇所などの情報が含まれていない。
anyhow::Context
トレイトをインポートとすると、Result
およびOption
でcontext
, 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_context
はResult
だけでなくOption
からも呼べる。None
の場合にanyhow::Error
をreturn
する。
以下の例では最後にchecked_add
で100
を足している。checked_add
はオーバーフローしたときにNone
を返す。
Result
のエラーもOption
のNone
も呼び出し元に委譲できる。
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_string
とparse
のエラーについては上の例と同じなのでis_err
でエラーであることの確認のみをしている。
その他の例は以下の記事を参照。
- 関連記事: RustでOptionをResultに変換
マクロ(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のマクロについての詳細は以下の記事を参照。