【Rust】anyhow で複数のエラーをまとめて扱う方法

Rust で anyhow を用いて複数のエラーをまとめて扱う方法について、初心者にも分かりやすく解説します。
anyhow クレートの概要
anyhow とは
Rust のエラー処理では、Result<T, E> 型を使用して「成功(Ok)」か「失敗(Err)」かを表現するのが基本となります。この時、E は、エラーを表す型の型パラメータです。
エラーの型は、各種外部のクレートなど様々な機能でも個別に用意されており、扱うエラー型は多岐に渡るため、すべてのエラー型を意識して厳密にエラーに対する処理を実装していくことは、多くの負担がかかってしまいます。
anyhow は、こうした「複数のエラーをまとめて扱いたい」場面で非常に便利なクレートです。anyhow::Error という汎用的なエラー型を用いることで、アプリケーション側のエラー処理を簡潔に表現して扱うことができます。
anyhow の特徴(複数のエラーをまとめて扱う)
anyhow を使用する最大のメリットは、エラー型を 1 つに統一できることです。
通常、Rust の ? 演算子は、エラーが一致 または 変換できる場合(From トレイトによる変換が可能な場合)にしか使用することができません。そのため、複数のエラー型が混ざる処理では、エラー変換を自前で実装したり、独自エラー型にまとめる必要性が出てきます。
anyhow を使用すると以下のような書き方ができるようになります。
- 関数の戻り値を
anyhow::Result<T>※にする。 - エラー型の種類を意識せずに
?で上位の呼び出し元に伝播することができる。
なお、anyhow はエラーを握りつぶすためのものではありません。後ほど説明するようにエラーに追加の文脈情報(コンテキスト)を付与することで、原因調査をしやすくするための仕組みも提供します。
anyhow の位置づけ(Result<T, E> や thiserror との関係性)
Rust のエラー処理では、基本的に Result<T, E> 型を利用してエラーを処理します。この E は、エラーのためのジェネリックな型パラメータで、任意の型を使用することができます。
anyhow を使用するということは、E に anyhow::Error を採用する設計を意味します。とにかくエラー型を統一して扱いたい場合には、anyhow は非常に向いています。
一方で、thiserror という独自エラー型を設計するクレートも非常によく使用されます。これは、自身で MyError 型を定義し、Result<T, MyError> のようにエラーの種類を明確に表現したい場合に向いています。クレートの位置づけを整理すると以下の通りです。
thiserror:独自のエラー型を簡単に定義する。anyhow:様々なエラー型をまとめて取り扱うことができる。
一般的には、ライブラリ側では thiserror、アプリケーション側では anyhow が使われることが多いです。ライブラリが用意している独自エラー型を、アプリケーション側が受け取った際には anyhow::Error に変換・統一して扱うという構成です。もちろん、独自エラーの情報を消してしまうわけではなく、元の独自エラーの情報を保持しつつ、anyhow::Error に統一します。
anyhow の基本
anyhow の導入方法
anyhow は、他クレートと同様に cargo add または Cargo.toml に追記することで利用できるようになります。
【cargo add で追加する場合】
cargo add anyhow
【Cargo.toml に追記する場合】
[dependencies] anyhow = "1.0.101"
例での記載バージョンは、記事執筆・更新時点での最新バージョンで記載しているため、導入時には crates.io の anyhow のページを確認して指定してください。
? で複数のエラーをまとめて扱う基本的な使い方
anyhow を利用する最も大きなメリットの 1 つは、ライブラリなどの複数のエラーをまとめて扱うことができることです。代表的な使い方として ? 演算子により、From トレイトによる変換を通じて、外部エラーを anyhow::Error に変換して扱う方法を紹介します。
以下は、input_file.txt を読み込む際に、ファイルが存在しない場面と思ってください。
use anyhow::Result;
use std::fs;
fn main() -> Result<()> {
let path = "input_file.txt";
// エラーを anyhow::Error に変換して返す
let content = fs::read_to_string(path)?;
println!("{content}");
Ok(())
}【実行結果】 Error: 指定されたファイルが見つかりません。 (os error 2) error: process didn't exit successfully: `D:\RustProject\rust-tech-sample-source\target\debug\examples\anyhow_basic.exe` (exit code: 1)
例では、fs::read_to_string によって std::io::Error が発生しています。通常であれば、このエラー型にあわせて処理を実装する必要があります。
しかし、main 関数の戻り値は anyhow::Result<()>(= Result<(), anyhow::Error>)としているため、自動的に anyhow::Error に変換され、? 演算子により上位へ返却されます。このように、エラー型を意識せずに ? を使えることが、anyhow を使用する大きなメリットです。
context / with_context で文脈情報(コンテキスト)を追加する
? を使用したエラー伝播は非常に簡潔で便利ですが、そのままではどの処理で失敗したのかが分かりづらくなってしまう場合があります。このような場合に便利なのが、Context トレイトが提供する context および with_context です。
これらを使うことで、エラー発生時に追加の文脈情報(コンテキスト)を付与することができ、原因調査をしやすくなります。
context で固定のメッセージを追加する
context を使用することで以下のように固定のメッセージを追加することができます。
use anyhow::{Context, Result};
use std::fs;
fn main() -> Result<()> {
let path = "input_file.txt";
// コンテキスト情報を追加してエラーを返す
let content =
fs::read_to_string(path).context("[main] テキストファイルの読み込みに失敗しました。")?;
println!("{content}");
Ok(())
}【実行結果】
Error: [main] テキストファイルの読み込みに失敗しました。
Caused by:
指定されたファイルが見つかりません。 (os error 2)
error: process didn't exit successfully: `D:\RustProject\rust-tech-sample-source\target\debug\examples\anyhow_context.exe` (exit code: 1)例のように context を使用すると、元のエラーに対して固定のメッセージを追加することができます。実行結果を見ると、最初に追加したメッセージが表示されており、その下の「Caused by:」以下に元のエラーが保持されていることが分かります。
このように、anyhow は元のエラー情報を失うことなく、追加の文脈情報(コンテキスト)を積み重ねて保持することができます。
with_context で動的にメッセージを生成して追加する
context は固定メッセージを追加する方法でしたが、with_context を使用すると、以下のように動的にメッセージを生成して追加することができます。
use anyhow::{Context, Result};
use std::fs;
fn main() -> Result<()> {
let path = "input_file.txt";
// クロージャーを使って動的にコンテキスト情報を追加してエラーを返す
let content = fs::read_to_string(path)
.with_context(|| format!("[main] テキストファイルの読み込みに失敗しました。path={path}"))?;
println!("{content}");
Ok(())
}【実行結果】
Error: [main] テキストファイルの読み込みに失敗しました。path=input_file.txt
Caused by:
指定されたファイルが見つかりません。 (os error 2)
error: process didn't exit successfully: `D:\RustProject\rust-tech-sample-source\target\debug\examples\anyhow_with_context.exe` (exit code: 1)with_context は、クロージャーを受け取り、エラーが発生した場合にのみ処理を実行します。そのため、例のように format! マクロを使って変数を埋め込んだ動的なメッセージを生成したい場合などに向いています。
context と with_context は似ていますが、使い分けは以下のように整理できます。
context:固定のメッセージを追加したい場合with_context:エラー発生時に処理を行い、動的なメッセージを生成したい場合
anyhow! / bail! マクロで自分でエラーを作る
これまでは、下位の処理が返却するエラーを ? 演算子で伝播する例を見てきました。一方で、アプリケーション側の条件チェックなどにより自分でエラーを生成したい場合もよくあります。
このような場合には、anyhow! マクロや bail! マクロの使用が便利です。
anyhow! マクロで anyhow::Error を生成する
anyhow! マクロを使用することで以下のように anyhow::Error を生成することができます。
use anyhow::{Result, anyhow};
fn main() -> Result<()> {
let path = "";
if path.is_empty() {
// エラーを生成する
let err = anyhow!("[main] パスが空です。");
// エラーを返却する
return Err(err);
}
Ok(())
}【実行結果】 Error: [main] パスが空です。 error: process didn't exit successfully: `D:\RustProject\rust-tech-sample-source\target\debug\examples\anyhow_anyhow_macro.exe` (exit code: 1)
例のように、anyhow! マクロにより anyhow::Error 型の値を生成することができ、変数に代入して扱うことができます。これにより、ログ出力などの追加の処理をしてから「return Err(err)」のように返却するといったことが可能になります。
bail! マクロで anyhow::Error を生成し、即時返却する
anyhow! マクロでは一度変数に代入して扱いましたが、場合によってはエラー発生時に即時に return すればいい場合があります。このような場合には、bail! マクロを使用することでコードがより簡潔に書くことができます。
use anyhow::{Result, bail};
fn main() -> Result<()> {
let path = "";
if path.is_empty() {
// エラーを生成して、即時に返却する
bail!("[main] パスが空です。");
}
Ok(())
}【実行結果】 Error: [main] パスが空です。 error: process didn't exit successfully: `D:\RustProject\rust-tech-sample-source\target\debug\examples\anyhow_bail_macro.exe` (exit code: 1)
例のように bail! マクロは anyhow::Error を生成し、その場で Err を返却しています。そのため、return の記載も必要なくなり非常に簡潔にコードを各ことができます。
まとめ
Rust で anyhow を用いて複数のエラーをまとめて扱う方法について解説しました。
anyhow を使用することでアプリケーション側ではエラー型を 1 つに統一し、? 演算子によるエラー伝播を簡潔に記述できます。
また、context / with_context を使用することでエラーに追加の文脈情報(コンテキスト)を追加することで原因調査もしやすくなります。さらに、anyhow! や bail! といったマクロを使うことで、自分でエラー生成をして扱うことも簡単です。
anyhow は、エラー型を厳密にするためのクレートではなく、アプリケーション側でのエラー運用をしやすくしてくれるクレートです。ライブラリ側では、thiserror による独自エラー型を設計し、アプリケーション側では anyhow を使って統一的にエラーを扱うという使い分けをすると非常に効率的に開発を進められると思います。
ぜひ、anyhow の使い方の基本をしっかりと覚えてもらえたらと思います。

