【Rust入門】エラー処理の基本を分かりやすく解説

Rust のエラー処理の基本について初心者にも分かりやすく解説します。
Rust におけるエラー処理の考え方
どのようなプログラミング言語でもエラーが発生した際にどのように対処するのは非常に重要な設計事項です。多くのモダンなプログラミング言語では、例外(exception
)を発生させ、適切に例外ハンドラがキャッチして安全に対処することが行われます。
しかし、Rust では、例外を採用せず Result<T, E>
型という結果型を用いた明示的なエラー処理を基本にします。値があるかどうかのみが分かればよいケースでは Option
型を使用する場合もよくあります。
多くの言語で採用されている例外を用いたエラー処理には、以下のような問題点があります。
- 例外は、通常の関数やメソッドの戻り値の型には現れずにコードを読むだけではどのような種類の例外が発生しうるのか分からない。
- プログラマが例外処理を適切に実施しないと重大な不具合につながってしまう。
- C/C++ 等の言語では、例外が発生した時にリソースの解放忘れがないか検討が必要となる。
- 例外は容易に上位へ伝播することができてしまうため、どこでキャッチして処理されるべきかが曖昧になりやすい。
上記に共通して言えることは、例外を使うことでプログラムの「不透明さ」や「予測困難さ」が生じてしまう点です。この点を Rust では「型による明示的なエラー処理」を行うことで安全性を高めています。
この記事では、Rust のエラー処理の基本について解説していきます。
Rust のエラー処理の基本
Rust では、Result<T, E>
型と Option<T>
型という非常に重要な Enum
があります。
Result<T, E>
型は「成功と失敗を表現する」型であり、処理が成功した際には Ok(T)
、処理が失敗した場合は Err(E)
となります。ここで、T
、E
は、ジェネリックの型パラメータで任意の型を入れることができます。
また、Option<T>
型は「値があるかないかを表現する」型で、値がある場合には Some(T)
、値がない場合には None
となります。
Rust での関数やメソッドで、Result
型や Option
型を返却値として設計することが一般的で、Rust のエラー処理では、これらの型に対して以下のようなパターンの処理を行います。
match
を使った基本的なエラー処理?
演算子を用いて処理を上位へ移譲するunwrap
/expect
で強制的に中身を取り出す
以降で Result
型と Option
型を使用して各パターンの例を解説していきます。
match
を使った基本的なエラー処理
Result
型や Option
型に対して、パターンマッチング(match
)を使用する基本的なエラー処理について説明します。パターンマッチの基本については「パターンマッチングの基本について分かりやすく解説」を参考にしてください。
Rust の match
では、全てのパターンに対する処理をすることをプログラマに強制するため、確実に全てのケースに対してどのような処理が行われうるかを表現することができます。
Result
型の例
以下は、値が範囲内に入っていたら値を 2
倍する double_if_in_range
関数の例です。この関数の戻り値は Result<i32, String>
となっており、値の範囲内なら 2
倍した値を、範囲外の場合はエラーとなるメッセージ文字列を返却します。
以下の例で val
の値を変更しながらコンパイル/実行をして試してみてください。
fn double_if_in_range(value: i32, min: i32, max: i32) -> Result<i32, String> { // 値が範囲に入っていたら 2 倍する if value >= min && value <= max { Ok(2 * value) } else { Err(String::from("値は範囲外です。")) } } fn main() { let val = 10; // 値を変更して試してみてください let min = 0; let max = 20; // match で Result 型の返却値を処理する match double_if_in_range(val, min, max) { Ok(n) => println!("計算結果は {} です。", n), Err(e) => println!("Error: {}", e), } }
【実行結果】(val=10, min=0, max=20 で範囲内の場合) 計算結果は 20 です。 【実行結果】(val=50, min=0, max=20 で範囲外の場合) Error: 値は範囲外です。
main
関数内では、match
で double_if_in_range
関数の戻り値を受けとり、Ok(n)
であれば計算結果の n
を取り出して表示しています。一方、範囲外の場合は、Err(e)
の e
にエラーメッセージが入ってくるため、取り出してエラーとして表示をしています。
このように、結果のパターンにあわせて呼び出し元が処理を実装します。
Option
型の例
Option
型についても、Result
の場合と考え方は同様です。以下の is_in_range
関数は、戻り値が Option<i32>
となっているので、値が範囲内であれば値をそのまま返し、値が範囲外の場合は None
を返却するようにしています。
fn is_in_range(value: i32, min: i32, max: i32) -> Option<i32> { // 値が範囲内に入っているか判定する if value >= min && value <= max { Some(value) } else { None } } fn main() { let val = 10; // 値を変更して試してみてください let min = 0; let max = 20; // match で Option 型の返却値を処理する match is_in_range(val, min, max) { Some(n) => println!("{} は範囲内です。", n), None => println!("値は範囲外です。"), } }
【実行結果】(val=10, min=0, max=20 で範囲内の場合) 10 は範囲内です。 【実行結果】(val=50, min=0, max=20 で範囲外の場合) 値は範囲外です。
main
関数内の match
では戻り値が Some(n)
であれば、値の n
を取り出し表示し、None
の場合は、値が範囲外である旨を表示しています。
このように、match
を使うことで戻り値の各パターンに対して処理を行うのが Rust におけるもっとも代表的なエラー処理の方法となります。
?
演算子を用いて処理を上位へ移譲する
Rust での基本的なエラー処理は、Result
型や Option
型に対する match
での処理ですが、エラーの内容をその場で処理するのではなく、上位の呼び出し元に返して処理を移譲したい場合があります。このような時に非常に簡単に記述することができるのが「?
演算子」です。
?
演算子は、Result
と Option
のどちらでも利用することができ「結果が Ok
や Some
であれば中身を取り出し、Err
や None
であれば、すぐに return
する」という処理を表現します。これによりコードを大幅に簡略化できます。
具体的な例で見てみましょう。
Result
型の例
以下の例は、上記で紹介した double_if_in_range
関数ですが、main
関数から直接呼び出すのではなく、run
関数を作成して double_if_in_range
を呼び出し、その結果を ?
演算子で処理しています。
これにより Ok
の場合は、run
関数内で値を取り出して処理をし、Err
の場合は処置を main
関数へ移譲しています。
fn double_if_in_range(value: i32, min: i32, max: i32) -> Result<i32, String> { // 処理内容は同じなので省略 } fn run() -> Result<i32, String> { let val = 10; // 値を変更して試してみてください let min = 0; let max = 20; // Ok が返ってきたら値を取り出し、Error の場合は即時 return する let result = double_if_in_range(val, min, max)?; println!("[run] 計算結果: {}", result); // 成功の場合は、Ok を返却 Ok(result) } fn main() { // 実行と結果の処理 match run() { Ok(_) => println!("[main] 成功"), Err(e) => println!("[main] 失敗: {}", e), } }
【実行結果】(val=10, min=0, max=20 で範囲内の場合) [run] 計算結果: 20 [main] 成功 【実行結果】(val=50, min=0, max=20 で範囲外の場合) [main] 失敗: 値は範囲外です。
?
演算子は「double_if_in_range(val, min, max)?
」のように、関数の後ろに「?
」を付けます。これにより Ok
が返ってきた場合は Ok
の中身を取り出して result
に設定できます。一方で、Err
の場合は、Err
の値をその場で main
関数へ return
します。
main
関数内では、run
の返却値をもとに match
で処理していることから、この例は、main
関数でエラー処理を集約しているような構成となります。
Option
型の例
?
演算子は、Option
型に対しても使用できます。Option
に対して ?
演算子を使うと、Some
の場合は中の値を取り出し、None
の場合は、そのまま None
を上位へ返却します。
fn is_in_range(value: i32, min: i32, max: i32) -> Option<i32> { // 処理内容は同じなので省略 } fn run() -> Option<i32> { let val = 10; // 値を変更して試してみてください let min = 0; let max = 20; // Some が返ってきたら値を取り出し、Noneの場合は即時 return する let result = is_in_range(val, min, max)?; println!("[run] {} は範囲内です。", result); return Some(result); } fn main() { // 実行と結果の処理 match run() { Some(n) => println!("[main] 成功: {}", n), None => println!("[main] エラー: 値は範囲外です。"), } }
【実行結果】(val=10, min=0, max=20 で範囲内の場合) [run] 10 は範囲内です。 [main] 成功: 10 【実行結果】(val=50, min=0, max=20 で範囲外の場合) [main] エラー: 値は範囲外です。
これにより、Result
の場合と同様に None
の場合の処置を main
関数へ移譲することが可能になります。
main
関数で ?
演算子を使用して Result
を返却する
プログラムを開始する main
関数でも ?
演算子を使用することが可能です。この場合、Err
のの内容を表示してプログラムを終了します。
Result
型の例
use std::error::Error; fn double_if_in_range(value: i32, min: i32, max: i32) -> Result<i32, String> { // 処理内容は同じなので省略 } fn main() -> Result<(), Box<dyn Error>> { let val = 10; // 値を変更して試してみてください let min = 0; let max = 20; // Ok が返ってきたら値を取り出し、Error の場合は即時 return する let result = double_if_in_range(val, min, max)?; println!("計算結果は {} です。", result); // 成功の場合は、Ok(())を返却 Ok(()) }
【実行結果】(val=10, min=0, max=20 で範囲内の場合) 計算結果は 20 です。 【実行結果】(val=50, min=0, max=20 で範囲外の場合) Error: "値は範囲外です。" error: process didn't exit successfully: `target\debug\examples\question_mark_operator_result.exe` (exit code: 1)
main 関数で Result を返却する場合には、返却値の型注釈が必要となります。上記例では「Result<(), Box<dyn Error>>
」としています。Box<dyn Error>
は、Error
トレイトを持つ任意の型を扱えるようにした表現ですが詳細説明は省略します。任意のエラー型をまとめて扱えるようにするための決まった書き方として覚えておいてください。
実行結果を見てみると、システム側でエラーを出力し、exit code 1
で終了していることが分かります。上記の方法は、小規模プログラムや学習用サンプルなどでは便利な方法ですが、エラー処理はプログラマの責任であるため、基本的には main
関数内で処理をするのが適切です。
Option
型の例
main
関数では、特別に Result
型を返却できるようになっていますが、Option
型を返却することはできません。
そのため、main
内で ?
演算子により Option
型を扱いたい場合は、ok_or
メソッドなどを使って Result
型に変換してから ?
演算子を使うことで上位への移譲を表現できます。
use std::error::Error; fn is_in_range(value: i32, min: i32, max: i32) -> Option<i32> { // 処理内容は同じなので省略 } fn main() -> Result<(), Box<dyn Error>> { let val = 10; // 値を変更して試してみてください let min = 0; let max = 20; // Some が返ってきたら値を取り出し、Noneの場合は Resultへ変換 する let result = is_in_range(val, min, max).ok_or("値は範囲外です。")?; println!("{} は範囲内です。", result); Ok(()) }
【実行結果】(val=10, min=0, max=20 で範囲内の場合) 10 は範囲内です。 【実行結果】(val=50, min=0, max=20 で範囲外の場合) Error: "値は範囲外です。" error: process didn't exit successfully: `target\debug\examples\question_mark_operator_option.exe` (exit code: 1)
ここまでで紹介してきた ?
演算子による処理は、Err
や None
に対する処理を関数の呼び出し元に移譲するということになるため、どのような選択とするかは設計者の判断が必要です。
例えば、リトライ処理などであれば、その関数内で match
により処理するのが適切だと思いますが、上位の制御の流れで判断するべきであれば上位に送るのがいいでしょう。十分に検討して使用するようにしてください。
unwrap
/ expect
で強制的に中身を取り出す
もう1つのエラー処理としては、unwrap
/ expect
メソッドを使って中身を強制的に取り出す方法です。これらのメソッドは、Result
の Ok
や Option
の Some
の場合は中身を取り出し、Err
や None
の場合には即座に panic
でプログラムを強制終了します。
expect
は、自分が指定したエラーメッセージを表示できる点で unwrap
と異なります。使い方を以下の例で見てみましょう。
Result
型の例
以下の例では、unwrap
メソッドを使用している例です。
fn double_if_in_range(value: i32, min: i32, max: i32) -> Result<i32, String> { // 処理内容は同じなので省略 } fn main() { let val = 10; // 値を変更して試してみてください let min = 0; let max = 20; // unwrap で Ok であれば値を取り出し、None の場合は panic で終了 let result = double_if_in_range(val, min, max).unwrap(); println!("計算結果は {} です。", result); }
【実行結果】(val=10, min=0, max=20 で範囲内の場合) 計算結果は 20 です。 【実行結果】(val=50, min=0, max=20 で範囲外の場合) thread 'main' panicked at examples\unwrap_result.rs:16:52: called `Result::unwrap()` on an `Err` value: "値は範囲外です。" note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace error: process didn't exit successfully: `target\debug\examples\unwrap_result.exe` (exit code: 101)
上記結果を見ると分かるようにエラーの場合は、panic
で処理が終了しています。エラーメッセージの中には、Err
の中身が表示されていることが分かります。
また、expect
を使用することにより特定の文字を出力に追加することが可能です。
// Ok であれば値を取り出し、None の場合は panic で終了 // expect によりエラーメッセージを追加 let result = double_if_in_range(val, min, max).expect("エラー発生"); println!("計算結果は {} です。", result);
【実行結果】(val=50, min=0, max=20 で範囲外の場合) thread 'main' panicked at examples\expect_result.rs:16:52: エラー発生: "値は範囲外です。" note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace error: process didn't exit successfully: `target\debug\examples\expect_result.exe` (exit code: 101)
Option
型の例
Option
に対しても同様に unwrap
を使用することができます。
fn is_in_range(value: i32, min: i32, max: i32) -> Option<i32> { // 処理内容は同じなので省略 } fn main() { let val = 50; // 値を変更して試してみてください let min = 0; let max = 20; // unwrap で Some であれば値を取り出し、None の場合は panic で終了 let result = is_in_range(val, min, max).unwrap(); println!("{} は範囲内です。", result); }
【実行結果】(val=10, min=0, max=20 で範囲内の場合) 10 は範囲内です。 【実行結果】(val=50, min=0, max=20 で範囲外の場合) thread 'main' panicked at examples\unwrap_option.rs:16:45: called `Option::unwrap()` on a `None` value note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace error: process didn't exit successfully: `target\debug\examples\unwrap_option.exe` (exit code: 101)
結果を確認すると None
で panic
となっていることがエラーメッセージから分かります。
expect
についても同様です。Option
は None
だと理由が不明確なのでエラーの理由を追加するのに役立ちます。
// Some であれば値を取り出し、None の場合は panic で終了 // expect によりエラーメッセージを追加 let result = is_in_range(val, min, max).expect("値は範囲外です。"); println!("{} は範囲内です。", result);
【実行結果】(val=50, min=0, max=20 で範囲外の場合) thread 'main' panicked at examples\expect_option.rs:17:45: 値は範囲外です。 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace error: process didn't exit successfully: `target\debug\examples\expect_option.exe` (exit code: 101)
Result
型と Option
型の使い分けの考え方
関数やメソッドを設計する際には、上記のように紹介したように返却値として Result
型や Option
型を使用することが一般的になります。どちらの型を使用するかは、設計時によく検討が必要です。
状況により設計の検討で必ずこうするべきという考え方があるわけではありませんが、一般的な考え方を紹介しておきます。
Result 型を使う場合
- 「値がない」という事実だけでなく、なぜ失敗しているかを利用者に伝えたいとき。
(例:ファイル読み込み、パース処理、通信結果など) - エラー原因を
Err
に含められるため、呼び出し側がロギングやリトライといったエラー処理をどのようにするかの判断したいとき。 - その他、呼び出し側が「失敗理由」を知る必要があるとき。
使用例としては、入出力やパース関連の処理で、失敗した際には Result
の Err
で具体的な原因を返すような場合です。
Option 型を使う場合
- 「値がない」という状況が正常なケースの1つであるとき。
(検索してヒットしない場合はNone
など) - 「失敗」ではなく「存在しない」と解釈できるとき。
- その他、呼び出し側が「値があるかどうか」だけが分かればよいとき。
使用例としては、検索関連のAPIで、Option
を利用し、検索がヒットしないときには None
を返すような場合です。
Result
を使うか、Option
を使うかは、実装するケースにあわせてどちらを使うか十分に検討するようにしましょう。
エラー処理を支える便利なクレート
Rust のエラー処理に関して上記で紹介したような基本の他に、実務でよく名前が挙がるのが「thiserror
」と「anyhow
」というクレートです。
thiserror
:独自のエラー型を簡単に定義することができます。anyhow
:様々なエラー型をまとめて取り扱うことができるようにします。
今回の記事は入門向け記事のため、上記のクレートについては紹介にとどめ、別途紹介する記事を記載しようかなと思います。
まとめ
Rust のエラー処理の基本について紹介しました。Rust は例外を持たず、Result
型と Option
型を用いた明示的なエラー処理を基本とします。
この記事では以下の例を紹介しています。
match
を使った基本的なエラー処理?
演算子を用いて処理を上位へ移譲するunwrap
/expect
で強制的に中身を取り出す
?
演算子や unwrap
/ expect
の使用については、小さなシステムなのではよく利用しますが、大規模なシステムでは慎重に判断が必要です。大規模なシステムでは、エラー型を適切に定義し、match
を使った明確な処理を記載する方が適切です。
その際には、thiserror
や anyhow
のようなより簡単にエラーを扱うためのクレートもありますが、この記事な入門ということで説明は省略しています。また、別途記事でまとめてみたいと思います。
Rust では、上記のような明示的なエラー処理を行うことで安全性を高めることができます。ぜひ使い方をしっかりと理解してもらいたいと思います。