header image

枝折

Rust でのエラーハンドリングいろいろ

CREATED: 2023 / 08 / 09 Wed

UPDATED: 2023 / 08 / 09 Wed

CLI アプリのドキュメントに載ってたエラーハンドリングをまとめてみました。

Rust での Results を使ったエラーハンドリング

ResultsOkErr を返す enum ですが、一般的にこれを使ってエラーを取り扱います、rust では。

例として、std::fs::read_to_string という関数があるのですが、これは指定されたパスが存在しない場合は Err を返し、正常に処理が終了すれば Ok を返します。

// main.rs
fn main() {
  let result = std::fs::read_to_string("test.txt");
  match result {
      Ok(content) => { println!("File content: {}", content); }
      Err(error) => { println!("Oh noes: {}", error); }
  }
}
$ rustc main.rs
$ ./main
Oh noes: No such file or directory (os error 2)

こんな感じでファイルのパスが存在しなければエラーを吐きます。 エラーは吐きますが、match の後に書いた処理は実行されます。

以上終了させたいのであれば panic! させます。

match result {
    Ok(content) => { println!("File content: {}", content); }
    Err(error) => { panic!("Oh noes: {}", error); }
}
$ ./main
thread 'main' panicked at 'Oh noes: No such file or directory (os error 2)', main.rs:5:23
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

そしたら match の後に書いた処理は実行されません。

match から結果を取り出したい

match の中でファイルの中身を取得できても、その外で扱いたいことがほとんどだと思います。 Ok の中で処理をするので十分な場合はこのままで良いですが、取得した中身を取り出してみましょう。

let result = std::fs::read_to_string("test.txt");
let content = match result {
    Ok(content) => { content },
    Err(error) => { panic!("Can't deal with {}, just exit here", error); }
};
println!("file content: {}", content);

こうすれば content に中身が渡って下の println! でその内容が表示されるようになります。 もちろん Err の場合は異常終了します。

または、このコードは以下のように書いても同じ挙動をとります。

let content = std::fs::read_to_string("test.txt").unwrap();
println!("file content: {}", content);

すっきりしました。 お好みでどうぞ。

でも panic! させたくない

場合によってはエラーが発生しても異常終了せずに、後続の処理に結果を活かしたい場合もあるかもしれません。 以下のようにすると、この処理が実行される関数から Err が返却されます。

let result = std::fs::read_to_string("test.txt");
let content = match result {
    Ok(content) => { content },
    Err(error) => { return Err(error.into()); }
};

するとこのコードをコンパイルする時に return type の問題で怒られるかもしれません。 Err なんて返すなと。

そしたらこうしてあげれば良いです。

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let result = std::fs::read_to_string("test.txt");
    let content = match result {
        Ok(content) => { content },
        Err(error) => { return Err(error.into()); }
    };
    println!("file content: {}", content);
    Ok(())
}

return typeResult を指定してあげれば、Err が返ろうが Ok が返ろうが問題ありません。 ファイルの内容が取得されれば println! で出力されますし、とりあえずこれで良さそうです。 これでこの関数の戻り値を受け取る側で何かしらの処理を加えることができるでしょう。

ちなみに最終行の Ok(()) は return Ok(()) の短縮形です。

クエスチョンマークを使った panic! 回避

もっと簡潔に書きたい場合はクエスチョンマークを使った記法もあります。

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let content = std::fs::read_to_string("test.txt")?;
    println!("file content: {}", content);
    Ok(())
}

これも同様に問題があれば Err を返却し、正常に終了すれば Ok(()) が返却されます。

エラー内容にコンテクストを付け加えたい

エラーがどのような理由で発生したのか、その前後関係を補足したい時があります。 上記のコードを実行したときも Not Found エラーが出ているものの、じゃあ何が Not Found になっているのかがわからなかったかと思います。 こういった具体的な問題の内容を知ることができれば大変嬉しいものです。

そんな時にはカスタムのエラー構造体を作りましょう。

struct CustomError(String);

こんな感じのカスタムエラー構造体を以下のように利用すれば、エラーの内容をより具体的に表現することができます。

fn main() -> Result<(), CustomError> {
    let path = "test.txt";
    let content = std::fs::read_to_string(path)
        .map_err(|err| CustomError(format!("Error reading `{}`: {}", path, err)))?;
    println!("file content: {}", content);
    Ok(())
}
Error: CustomError("Error reading `test.txt`: No such file or directory (os error 2)")

このカスタムエラー構造体を定義してエラーメッセージを具体的に出力するマナーはよくあるらしいです。 ただまあ、より CLI ツールのエラーっぽく出力したい場合は、anyhow というライブラリを使うことでいい感じにしてくれます。

anyhow の Context トレイトによって出力がいい感じに見通しよく表示されます。

use anyhow::{Context, Result};

fn main() -> Result<()> {
    let path = "test.txt";
    let content = std::fs::read_to_string(path)
        .with_context(|| format!("could not read file `{}`", path))?;
    println!("file content: {}", content);
    Ok(())
}
Error: could not read file `test.txt`

Caused by:
    No such file or directory (os error 2)

を仕舞い

このページのを砕いて書きました。