It’s now or never

IT系の技術ブログです。気になったこと、勉強したことを備忘録的にまとめて行きます。

RustによるResult型のエラーハンドリングの入門

RustにおけるResultを使った、エラーハンドリングの基本的な部分についてまとめておく。

Result型

doc.rust-lang.org

Rustには、エラーハンドリングのための特別な型、Resultが用意されている。

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Okバリアントは操作が成功した場合に使用され、Errバリアントはエラーが発生した場合に使用される。

[例]

use std::fs;
use std::io;

fn read_file_content(file_path: &str) -> Result<String, io::Error> {
    fs::read_to_string(file_path)
}

fn main() {
    match read_file_content("path/to/your/file.txt") {
        Ok(content) => println!("File content: {}", content),
        Err(err) => eprintln!("Failed to read the file: {}", err),
    }
}

上記はファイルの読み込みのサンプル。

特定のファイルを文字列として読み込む関数(fs::read_to_string)は、Result型を返す。

? 演算子

doc.rust-lang.org

Rustには、多言語における例外のthrowなどはなく基本は関数(メソッド)のReturnをハンドリングすることになる。(主にResultやOption)

多くの関数では、ハンドリングするエラーが発生することがあり、毎回ResultのOk/Errをハンドリングすると処理が煩雑になってしまうことが考えられる。このような問題の対策として、Rustには ? 演算子が用意されている。

?演算子は、Resultを簡単に扱うための構文糖衣で、関数がResult型を返す場合、?を使うことでエラーが発生したら早期にその関数からリターンすることができる。

例えば、?演算子を使わない場合は以下のようにmatchなどを使ってハンドリングする必要がある。

【?演算子を使用しない】

fn func1() -> Result<String, &'static str> {
    // 戻り値のResultをハンドリングする必要がある
    match func2() {
        Ok(val2) => match func3(val2) {
            Ok(val3) => func4(val3),
            Err(e) => Err(e),
        },
        Err(e) => Err(e),
    }
}

fn func2() -> Result<String, &'static str> {
    Ok("func2".to_string())
}

fn func3(val: String) -> Result<String, &'static str> {
    Ok(format!("{} -> func3", val))
}

fn func4(val: String) -> Result<String, &'static str> {
    Ok(format!("{} -> func4", val))
}

fn main() {
    match func1() {
        Ok(result) => println!("Result: {}", result),
        Err(e) => println!("Error: {}", e),
    }
}

【?演算子を使用する】

fn func1() -> Result<String, &'static str> {
    // ?演算子を使う場合Errの場合は、その時点でResult(Err)が返される
    let val2 = func2()?;
    let val3 = func3(val2)?;
    func4(val3)
}

・・・

fn main() {
    match func1() {
        Ok(result) => println!("Result: {}", result),
        Err(e) => println!("Error: {}", e),
    }
}

?演算子は、同じErrの型のみ返すことができる

?演算子をある関数内で使用するときは、呼び出す関数のResultの型と?演算子で呼び出す関数のErrの型が同じでなくてはならない。(内部でリターンしているので当然ではあるが)

【正しいパターン】

use std::fs;
use std::io;

fn read_file1() -> Result<String, io::Error> {
    fs::read_to_string("path/to/file.txt")
}

fn read_file2() -> Result<String, io::Error> {
    fs::read_to_string("path/to/file2.txt")
}

fn function() -> Result<(), io::Error> {
    // 両方ともio::Errorを返すため問題ない
    let _content = read_file1()?;
    let _content = read_file2()?;
    Ok(())
}

コンパイラエラーのパターン】

fn read_file() -> Result<String, io::Error> {
    fs::read_to_string("path/to/file.txt")
}

fn another_task() -> Result<(), &'static str> {
    Err("This is a different error type")
}

fn function() -> Result<(), io::Error> {
    let _content = read_file()?;
    // Errの型がstrなのでコンパイルが通らない
    another_task()?;
    Ok(())
}

カスタムエラーのハンドリング

一つの関数の中で、ハンドリングするエラーの型が必ず同じものであることは稀のため、関数におけるエラーの型を独自で制御する必要が出てくる。

map_errを使ってエラー型を変換する

あるエラー型を別のエラー型に変換するために、map_errという関数が用意されている。

以下のサンプルのように、特定のError型(RustではEnumで作るのが慣習)に、 map_err 関数で変換することで?演算子を使ってスリムに処理を書くことができる。

use std::fs;
use std::io;

#[derive(Debug)]
enum CustomError {
    Io(io::Error),
    Str(&'static str),
}

fn read_file() -> Result<String, io::Error> {
    fs::read_to_string("path/to/file.txt")
}

fn another_task() -> Result<(), &'static str> {
    Err("This is a different error type")
}

fn function() -> Result<(), CustomError> {
    let _content = read_file().map_err(CustomError::Io)?;
    another_task().map_err(CustomError::Str)?;
    Ok(())
}

fn main() {
    match function() {
        Ok(_) => println!("Success"),
        Err(e) => println!("Error: {:?}", e),
    }
}

Fromトレイトを実装する

?演算子でエラーをハンドリングした時に特定のエラーに変換するために、From トレイトが用意されている。

このトレイトを実装することで、map_err関数を使わなくても、特定のカスタムエラー型へ変換することができる。

use std::fs;
use std::io;

#[derive(Debug)]
enum CustomError {
    Io(io::Error),
    Str(&'static str),
}

impl From<io::Error> for CustomError {
    fn from(err: io::Error) -> CustomError {
        CustomError::Io(err)
    }
}

impl From<&'static str> for CustomError {
    fn from(err: &'static str) -> CustomError {
        CustomError::Str(err)
    }
}

fn read_file() -> Result<String, io::Error> {
    fs::read_to_string("path/to/file.txt")
}

fn another_task() -> Result<(), &'static str> {
    Err("This is a different error type")
}

fn function() -> Result<(), CustomError> {
    let _content = read_file()?;
    another_task()?;
    Ok(())
}

fn main() {
    match function() {
        Ok(_) => println!("All good"),
        Err(e) => println!("Error: {:?}", e),
    }
}

anyhowを使ってエラーを扱う

ここまで、Rustの基本的なエラーハンドリングについて書いてきたが、多くのライブラリ関数のエラー型を全て変換することは、コストでもあるため問題解決のライブラリが用意されている。

github.com

anyhowクレートでは、異なるエラータイプを一つの型に変換するための機能が提供されており、anyhow::Resultを使うことでエラー型をanyhow::Errorにまとめることができる。

use anyhow::{anyhow, Context, Result};

fn read_file() -> Result<String> {
    // contextは、map_errのように特定のエラーをanyhow::Errorに変換する
    fs::read_to_string("path/to/file.txt").context("Unable to read file")
}

fn another_task() -> Result<()> {
    Err(anyhow!("This is a different error type"))
}

// Result<T, E = Error> なのでErrは指定しなければ anyhow::Error型になる
fn function() -> Result<()> {
    let _content = read_file()?;
    another_task()?;
    Ok(())
}

fn main() {
    match function() {
        Ok(_) => println!("Success"),
        Err(e) => println!("Error: {}", e),
    }
}

thiserrorを使ってカスタムエラーを一つにまとめる

anyhow::Errorにまとめることで、とりあえず何も考えずエラーを一つに簡単にまとめられる。

簡単なプログラムであればこれで十分でストレス値は大分減らすことができるが、複雑なアプリケーションを書くときは、カスタムエラーを関数ごとに適切にハンドリングしたくなるケースがある。

#[derive(Debug)]
enum CustomError {
    Io(io::Error),
    Str(&'static str),
}

カスタムエラーを?演算子でResultにわたすためには、Fromトレイトが必要だが、これを簡単に記載できるthiserrorというクレートがある。

github.com

thiserrorはstderrorErrorの実装をマクロ化して便利に提供してくれているクレートで、この中にはFromトレイトのマクロもある。

#[derive(Debug, thiserror::Error)]
enum Error {
    #[error("Failed io error] {}", .0)]
    IoError(#[from] io::Error),
    #[error("{}", .0)]
    Unknown(#[from] anyhow::Error),
}

thiserror::Error を使うことで、Fromトレイトの実装をマクロで記載できるため実装がスリムになる。

use anyhow::{anyhow, Result};

use std::fs;
use std::io;

#[derive(Debug, thiserror::Error)]
enum CustomError {
    #[error("Failed io error] {}", .0)]
    IoError(#[from] io::Error),
    #[error("{}", .0)]
    Unknown(#[from] anyhow::Error),
}

fn function() -> Result<String, CustomError> {
    let file = fs::read_to_string("credentials.json")?;
    another_task()?;
    Ok(file)
}

fn another_task() -> Result<(), anyhow::Error> {
    Err(anyhow!("This is a different error type"))
}

fn main() {
    match function() {
        Ok(_) => println!("Function completed successfully"),
        Err(e) => match e {
            CustomError::IoError(io_err) => println!("IO error occurred: {}", io_err),
            CustomError::Unknown(anyhow_err) => {
                println!("An unknown error occurred: {}", anyhow_err)
            }
        },
    }
}

このように anyhow, thiserrorを使うと自前のトレイト実装が極力減らせ、?演算子を使ってストレスなくコーディングができる。

ここでは記載していないが、stderrorError のsourceや、std::fmt::Display のマクロを使って簡単にカスタムエラーを定義することもできる。