Rust入門

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

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

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) となります。ここで、TEは、ジェネリックの型パラメータで任意の型を入れることができます。

また、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 関数内では、matchdouble_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入門】列挙型の基本を分かりやすく解説

【Rust入門】パターンマッチングの基本について分かりやすく解説

? 演算子を用いて処理を上位へ移譲する

Rust での基本的なエラー処理は、Result 型や Option 型に対する match での処理ですが、エラーの内容をその場で処理するのではなく、上位の呼び出し元に返して処理を移譲したい場合があります。このような時に非常に簡単に記述することができるのが「? 演算子」です。

? 演算子は、ResultOption のどちらでも利用することができ「結果が OkSomeであれば中身を取り出し、ErrNone であれば、すぐに 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)

ここまでで紹介してきた ? 演算子による処理は、ErrNoneに対する処理を関数の呼び出し元に移譲するということになるため、どのような選択とするかは設計者の判断が必要です。

例えば、リトライ処理などであれば、その関数内で match により処理するのが適切だと思いますが、上位の制御の流れで判断するべきであれば上位に送るのがいいでしょう。十分に検討して使用するようにしてください。

unwrap / expect で強制的に中身を取り出す

もう1つのエラー処理としては、unwrap / expect メソッドを使って中身を強制的に取り出す方法です。これらのメソッドは、ResultOkOptionSome の場合は中身を取り出し、ErrNone の場合には即座に 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)

結果を確認すると Nonepanic となっていることがエラーメッセージから分かります。

expect についても同様です。OptionNone だと理由が不明確なのでエラーの理由を追加するのに役立ちます。

    // 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)

unwrap / expect の利用場面

unwrapexpect は簡単に結果の処理ができるため、学習や試作コード、テストでよく使いますが、ErrNone の際に panic を引き起こしてしまうため、本番のコードでは避けるべきということを覚えておきましょう

Result 型と Option 型の使い分けの考え方

関数やメソッドを設計する際には、上記のように紹介したように返却値として Result 型や Option 型を使用することが一般的になります。どちらの型を使用するかは、設計時によく検討が必要です。

状況により設計の検討で必ずこうするべきという考え方があるわけではありませんが、一般的な考え方を紹介しておきます。

Result 型を使う場合

  • 「値がない」という事実だけでなく、なぜ失敗しているかを利用者に伝えたいとき。
    (例:ファイル読み込み、パース処理、通信結果など)
  • エラー原因を Err に含められるため、呼び出し側がロギングやリトライといったエラー処理をどのようにするかの判断したいとき。
  • その他、呼び出し側が「失敗理由」を知る必要があるとき。

使用例としては、入出力やパース関連の処理で、失敗した際には ResultErr で具体的な原因を返すような場合です。

Option 型を使う場合

  • 「値がない」という状況が正常なケースの1つであるとき。
    (検索してヒットしない場合は None など)
  • 「失敗」ではなく「存在しない」と解釈できるとき。
  • その他、呼び出し側が「値があるかどうか」だけが分かればよいとき。

使用例としては、検索関連のAPIで、Option を利用し、検索がヒットしないときには None を返すような場合です。

Result を使うか、Option を使うかは、実装するケースにあわせてどちらを使うか十分に検討するようにしましょう。

エラー処理を支える便利なクレート

Rust のエラー処理に関して上記で紹介したような基本の他に、実務でよく名前が挙がるのが「thiserror」と「anyhow」というクレートです。

  • thiserror:独自のエラー型を簡単に定義することができます。
  • anyhow:様々なエラー型をまとめて取り扱うことができるようにします。

今回の記事は入門向け記事のため、上記のクレートについては紹介にとどめ、別途紹介する記事を記載しようかなと思います。

まとめ

Rust のエラー処理の基本について紹介しました。Rust は例外を持たず、Result 型と Option 型を用いた明示的なエラー処理を基本とします。

この記事では以下の例を紹介しています。

  • match を使った基本的なエラー処理
  • ? 演算子を用いて処理を上位へ移譲する
  • unwrap / expect で強制的に中身を取り出す

? 演算子や unwrap / expect の使用については、小さなシステムなのではよく利用しますが、大規模なシステムでは慎重に判断が必要です。大規模なシステムでは、エラー型を適切に定義し、match を使った明確な処理を記載する方が適切です。

その際には、thiserroranyhow のようなより簡単にエラーを扱うためのクレートもありますが、この記事な入門ということで説明は省略しています。また、別途記事でまとめてみたいと思います。

Rust では、上記のような明示的なエラー処理を行うことで安全性を高めることができます。ぜひ使い方をしっかりと理解してもらいたいと思います。

ABOUT ME
ホッシー
ホッシー
システムエンジニア
はじめまして。当サイトをご覧いただきありがとうございます。
私は製造業のメーカーで、DX推進や業務システムの設計・開発・導入を担当しているシステムエンジニアです。これまでに転職も経験しており、以前は大手電機メーカーでシステム開発に携わっていました。

これまでの業務を通じてさまざまなプログラミング言語や技術に触れてきましたが、その中でもRustの設計思想に惹かれ、この言語についてもっと深く学びたい、そしてその魅力を発信していきたいと思い、このサイトを立ち上げました。

自身の学びを整理しつつ、同じようにRustに興味を持つ方のお役に立てるような情報を発信していければと思っています。どうぞよろしくお願いいたします。

※キャラクターデザイン:ゼイルン様
記事URLをコピーしました