【Rust】anyhowを使ったエラーハンドリング

anyhow = “1.0.97”
https://docs.rs/anyhow/latest/anyhow/

anyhowを使うと、エラー処理が簡単にできるようになる。
anyhow::Errorはエラー型のラッパー、あらゆるエラー型からanyhow::Errorに変換できる。変換できる。
Resultにすると、?演算子で異なる複数の型のエラーを呼び出し元に移譲できる。
contextやwith_contextを使うと、Noneをanyhow::Errorとして移譲することも可能
anyhow::ResultがResultの型エイリアスとして定義されているので、use anyhow::Result;とすると、ResultをResultと書ける。

use anyhow::{ensure, Context, Result};
use std::fs;

#[tokio::main]
async fn main()  {

    let path_123 = "./data/temp/123.txt";
    println!("{}", with_unwrap(&path_123));
    
    // let path_abc = "./data/temp/abc.txt";
    // println!("{}", with_unwrap(&path_abc));

    let path_non_existent = "./data/to/non_existent.txt";
    println!("{}", with_unwrap(&path_non_existent));
}

fn with_unwrap(path: &str) -> i32 {
    let s = fs::read_to_string(path).unwrap();
    let i = s.parse::<i32>().unwrap();

    i * 10
}

Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.76s
Running `target/debug/app`
1230
thread ‘main’ panicked at src/main.rs:16:30:
called `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: “No such file or directory” }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
vagrant@vagrant:~/dev/rust/app$

エラーメッセージが、InvalidDigit、No such file or directoryでそれぞれ異なる。

#[tokio::main]
async fn main()  {

    let path_123 = "./data/temp/123.txt";
    println!("{}", with_unwrap(&path_123).unwrap());
    
    let path_abc = "./data/temp/abc.txt";
    println!("{:?}", with_unwrap(&path_abc).unwrap_err());

    let path_non_existent = "./data/to/non_existent.txt";
    println!("{:?}", with_unwrap(&path_non_existent).unwrap_err());
}

fn with_unwrap(path: &str) -> Result<i32> {
    let s = fs::read_to_string(path)?;
    let i = s.parse::<i32>()?;

    Ok(i * 10)
}

1230
invalid digit found in string
No such file or directory (os error 2)

### anyhow::Contextでコンテキストを追加
with_contextは遅延評価でエラーの時のみ指定

fn with_unwrap(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)
}

1230
Failed to parse “abc”

Caused by:
invalid digit found in string
Failed to read “./data/to/non_existent.txt”

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

### OptionをResultに変換
contextおよび with_contextはResultだけでなく、Optionからも呼べる。Noneも呼び出し元に移譲できる。

async fn main()  {

    let path_max = "data/temp/max.txt";
    fs::write(path_max, i32::MAX.to_string()).unwrap();
    println!("{:?}", with_anyhow_option(path_max).unwrap_err());

    let path_123 = "./data/temp/123.txt";
    println!("{}", with_unwrap(&path_123).unwrap());
    
    let path_abc = "./data/temp/abc.txt";
    println!("{:?}", with_unwrap(&path_abc).is_err());

    let path_non_existent = "./data/to/non_existent.txt";
    println!("{:?}", with_unwrap(&path_non_existent).is_err());
}

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

Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.86s
Running `target/debug/app`
Overflow occurred
1230
true
true

### マクロ(anyhow!, bail!, ensure!)で単発のエラーを返す
ensureで値に応じてエラーとすることもできる。

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

match resultでの処理

    let path_123 = "./data/temp/123.txt";
    let result = with_anyhow_macro(&path_123);
    match result {
        Ok(n) => {
            println!("{}", n);
        }
        Err(error) => {
            println!("{}", error);
        }
    }

Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.81s
Running `target/debug/app`
Value must be at least 1000, got 223