2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rustのエラー処理を極める:ResultとOptionを超えて

Posted at

表紙

エラー処理に関するメソッド

エラーをスムーズに処理するには、公式が提供するメソッドを適切に活用することが重要です。うまく活用すれば効率が大幅に向上します。

  • or()
  • and()
  • or_else()
  • and_then()
  • map()
  • map_err()
  • map_or()
  • map_or_else()
  • ok_or()
  • ok_or_else()

以下では、これらのメソッドをどのような場面で使うべきか、どのように使うのかを詳しく説明し、最終的に Err 型を設計する際のポイントについても紹介します。

or(), and()

or()and() は、どちらか一方を選択する際に使用します。論理演算の OR(または)と AND(かつ)に似ています。

  • or() は、式を順番に評価し、いずれかの式の結果が Some または Ok であれば、その値を即座に返します。
  • and() は、両方の式の結果が Some または Ok の場合に、2 つ目の式の値を返します。どちらか一方が None または Err の場合は、その値を即座に返します。
let s1 = Some("some1");
let s2 = Some("some2");
let n: Option<&str> = None;

let o1: Result<&str, &str> = Ok("ok1");
let o2: Result<&str, &str> = Ok("ok2");
let e1: Result<&str, &str> = Err("error1");
let e2: Result<&str, &str> = Err("error2");

assert_eq!(s1.or(s2), s1); // Some1 or Some2 = Some1
assert_eq!(s1.or(n), s1); // Some or None = Some
assert_eq!(n.or(s1), s1); // None or Some = Some
assert_eq!(n.or(n), n); // None1 or None2 = None2

assert_eq!(o1.or(o2), o1); // Ok1 or Ok2 = Ok1
assert_eq!(o1.or(e1), o1); // Ok or Err = Ok
assert_eq!(e1.or(o1), o1); // Err or Ok = Ok
assert_eq!(e1.or(e2), e2); // Err1 or Err2 = Err2

assert_eq!(s1.and(s2), s2); // Some1 and Some2 = Some2
assert_eq!(s1.and(n), n); // Some and None = None
assert_eq!(n.and(s1), n); // None and Some = None
assert_eq!(n.and(n), n); // None1 and None2 = None1

assert_eq!(o1.and(o2), o2); // Ok1 and Ok2 = Ok2
assert_eq!(o1.and(e1), e1); // Ok and Err = Err
assert_eq!(e1.and(o1), e1); // Err and Ok = Err
assert_eq!(e1.and(e2), e1); // Err1 and Err2 = Err1

or_else(), and_then()

or()and() は単純な選択をするだけで、値を変更することはできません。もしエラー時に異なる処理を実行したい場合は、クロージャ(無名関数)を使う必要があります。そのために or_else()and_then() を使用します。

// `or_else()` の Option 版
let s1 = Some("some1");
let s2 = Some("some2");
let fn_some = || Some("some2");

let n: Option<&str> = None;
let fn_none = || None;

assert_eq!(s1.or_else(fn_some), s1); // Some1 or_else Some2 = Some1
assert_eq!(s1.or_else(fn_none), s1); // Some or_else None = Some
assert_eq!(n.or_else(fn_some), s2); // None or_else Some = Some
assert_eq!(n.or_else(fn_none), None); // None1 or_else None2 = None2

// `or_else()` の Result 版
let o1: Result<&str, &str> = Ok("ok1");
let o2: Result<&str, &str> = Ok("ok2");
let fn_ok = |_| Ok("ok2");

let e1: Result<&str, &str> = Err("error1");
let e2: Result<&str, &str> = Err("error2");
let fn_err = |_| Err("error2");

assert_eq!(o1.or_else(fn_ok), o1); // Ok1 or_else Ok2 = Ok1
assert_eq!(o1.or_else(fn_err), o1); // Ok or_else Err = Ok
assert_eq!(e1.or_else(fn_ok), o2); // Err or_else Ok = Ok
assert_eq!(e1.or_else(fn_err), e2); // Err1 or_else Err2 = Err2

map()

ResultOption の値を変換する場合は map() を使用します。

let s1 = Some("abcde");
let s2 = Some(5);

let n1: Option<&str> = None;
let n2: Option<usize> = None;

let o1: Result<&str, &str> = Ok("abcde");
let o2: Result<usize, &str> = Ok(5);

let e1: Result<&str, &str> = Err("abcde");
let e2: Result<usize, &str> = Err("abcde");

let fn_character_count = |s: &str| s.chars().count();

assert_eq!(s1.map(fn_character_count), s2); // Some1 map = Some2
assert_eq!(n1.map(fn_character_count), n2); // None1 map = None2

assert_eq!(o1.map(fn_character_count), o2); // Ok1 map = Ok2
assert_eq!(e1.map(fn_character_count), e2); // Err1 map = Err2

map_err()

ResultErr の値を変更する場合は map_err() を使用します。

let o1: Result<&str, &str> = Ok("abcde");
let o2: Result<&str, isize> = Ok("abcde");

let e1: Result<&str, &str> = Err("404");
let e2: Result<&str, isize> = Err(404);

let fn_character_count = |s: &str| -> isize { s.parse().unwrap() };

assert_eq!(o1.map_err(fn_character_count), o2); // Ok1 map = Ok2
assert_eq!(e1.map_err(fn_character_count), e2); // Err1 map = Err2

map_or()

エラーが発生しない場合、デフォルト値を返す map_or() を使用できます。

const V_DEFAULT: u32 = 1;

let s: Result<u32, ()> = Ok(10);
let n: Option<u32> = None;
let fn_closure = |v: u32| v + 2;

assert_eq!(s.map_or(V_DEFAULT, fn_closure), 12);
assert_eq!(n.map_or(V_DEFAULT, fn_closure), V_DEFAULT);

map_or_else()

map_or() はデフォルト値を固定で指定しますが、map_or_else() を使えば、クロージャを使ってデフォルト値を動的に生成できます。

let s = Some(10);
let n: Option<i8> = None;

let fn_closure = |v: i8| v + 2;
let fn_default = || 1;

assert_eq!(s.map_or_else(fn_default, fn_closure), 12);
assert_eq!(n.map_or_else(fn_default, fn_closure), 1);

let o = Ok(10);
let e = Err(5);
let fn_default_for_result = |v: i8| v + 1;

assert_eq!(o.map_or_else(fn_default_for_result, fn_closure), 12);
assert_eq!(e.map_or_else(fn_default_for_result, fn_closure), 6);

ok_or()

OptionResult に変換する場合は ok_or() を使用します。

const ERR_DEFAULT: &str = "error message";

let s = Some("abcde");
let n: Option<&str> = None;

let o: Result<&str, &str> = Ok("abcde");
let e: Result<&str, &str> = Err(ERR_DEFAULT);

assert_eq!(s.ok_or(ERR_DEFAULT), o); // Some(T) -> Ok(T)
assert_eq!(n.ok_or(ERR_DEFAULT), e); // None -> Err(default)

ok_or_else()

ok_or() は固定のエラーメッセージを返しますが、ok_or_else() を使えばクロージャでエラーメッセージを動的に作成できます。

let s = Some("abcde");
let n: Option<&str> = None;
let fn_err_message = || "error message";

let o: Result<&str, &str> = Ok("abcde");
let e: Result<&str, &str> = Err("error message");

assert_eq!(s.ok_or_else(fn_err_message), o); // Some(T) -> Ok(T)
assert_eq!(n.ok_or_else(fn_err_message), e); // None -> Err(default)

Error の設計

初心者は Result の型の違いに苦しむことが多いですが、Result の型を明確に理解すれば、エラー処理で困ることはなくなります。

シンプルなカスタムエラー

通常、プログラムでは独自のエラー型を定義します。まずは最もシンプルな Result のカスタムエラーを作成しましょう。

use std::fmt;

#[derive(Debug)]
struct CustomError;

impl fmt::Display for CustomError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "An Error Occurred, Please Try Again!")
    }
}

fn make_error() -> Result<(), CustomError> {
    Err(CustomError)
}

fn main() {
    match make_error() {
        Err(e) => eprintln!("{}", e),
        _ => println!("No error"),
    }

    eprintln!("{:?}", make_error());
}

eprintln!println! と基本的に同じですが、標準エラー出力に出力されます。

カスタムエラーに DebugDisplay を実装することで、エラーを適切に表示できます。また、Box<dyn std::error::Error> に変換するためにも必要になります。

より複雑なカスタムエラー

現実のプロジェクトでは、エラーコードやエラーメッセージを含めることが多いです。

use std::fmt;

struct CustomError {
    code: usize,
    message: String,
}

impl fmt::Display for CustomError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let err_msg = match self.code {
            404 => "Sorry, Can not find the Page!",
            _ => "Sorry, something is wrong! Please Try Again!",
        };

        write!(f, "{}", err_msg)
    }
}

impl fmt::Debug for CustomError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "CustomError {{ code: {}, message: {} }}",
            self.code, self.message
        )
    }
}

fn make_error() -> Result<(), CustomError> {
    Err(CustomError {
        code: 404,
        message: String::from("Page not found"),
    })
}

fn main() {
    match make_error() {
        Err(e) => eprintln!("{}", e), // "Sorry, Can not find the Page!"
        _ => println!("No error"),
    }

    eprintln!("{:?}", make_error()); // "CustomError { code: 404, message: Page not found }"
}

このように DisplayDebug を手動実装することで、エラーの表現を自由にカスタマイズできます。

エラーの変換

通常、プログラムではさまざまなライブラリの Result を扱いますが、それらの Err 型は統一されていません。異なるエラー型を統一するために std::convert::From を利用します。

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

#[derive(Debug)]
struct CustomError {
    kind: String,
    message: String,
}

impl From<io::Error> for CustomError {
    fn from(error: io::Error) -> Self {
        CustomError {
            kind: String::from("io"),
            message: error.to_string(),
        }
    }
}

fn main() -> Result<(), CustomError> {
    let _file = File::open("nonexistent_file.txt")?;

    Ok(())
}

このコードでは、? を使うことで std::io::ErrorCustomError に自動変換しています。

より多くのエラー型を統一するには、さらに From を実装すれば OK です。

use std::fs::File;
use std::io::{self, Read};
use std::num;

#[derive(Debug)]
struct CustomError {
    kind: String,
    message: String,
}

impl From<io::Error> for CustomError {
    fn from(error: io::Error) -> Self {
        CustomError {
            kind: String::from("io"),
            message: error.to_string(),
        }
    }
}

impl From<num::ParseIntError> for CustomError {
    fn from(error: num::ParseIntError) -> Self {
        CustomError {
            kind: String::from("parse"),
            message: error.to_string(),
        }
    }
}

fn main() -> Result<(), CustomError> {
    let mut file = File::open("hello_world.txt")?;

    let mut content = String::new();
    file.read_to_string(&mut content)?;

    let _number: usize;
    _number = content.parse()?;

    Ok(())
}

Result の異なる Err 型を統一する方法

プログラム内で異なる Err 型が混在する場合、以下の 4 つの方法で統一できます。

  1. Box<dyn Error> を使用
  2. 独自のエラー型を定義
  3. thiserror を使用
  4. anyhow を使用

1. Box<dyn Error> を使用

std::error::Error トレイトを実装していれば、Box<dyn Error> で統一できます。

use std::fs::read_to_string;
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    let html = render()?;
    println!("{}", html);
    Ok(())
}

fn render() -> Result<String, Box<dyn Error>> {
    let file = std::env::var("MARKDOWN")?;
    let source = read_to_string(file)?;
    Ok(source)
}

この方法は簡単ですが、以下のデメリットがあります:

  • 動的ディスパッチ:型情報が失われ、パフォーマンスがわずかに低下する。
  • ヒープ割り当て:エラーをヒープに格納するため、オーバーヘッドが発生する。
  • 型安全性の低下:具体的なエラー型を扱いにくくなる。

2. 独自のエラー型を定義

エラーを列挙型(enum)で管理すれば、型安全性が向上します。

use std::fs::read_to_string;

fn main() -> Result<(), MyError> {
    let html = render()?;
    println!("{}", html);
    Ok(())
}

fn render() -> Result<String, MyError> {
    let file = std::env::var("MARKDOWN")?;
    let source = read_to_string(file)?;
    Ok(source)
}

#[derive(Debug)]
enum MyError {
    EnvironmentVariableNotFound,
    IOError(std::io::Error),
}

impl From<std::env::VarError> for MyError {
    fn from(_: std::env::VarError) -> Self {
        Self::EnvironmentVariableNotFound
    }
}

impl From<std::io::Error> for MyError {
    fn from(value: std::io::Error) -> Self {
        Self::IOError(value)
    }
}

impl std::error::Error for MyError {}

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            MyError::EnvironmentVariableNotFound => write!(f, "Environment variable not found"),
            MyError::IOError(err) => write!(f, "IO Error: {}", err.to_string()),
        }
    }
}

この方法は型安全ですが、やや冗長です。

3. thiserror を使用

thiserror を使うと、エラーの定義が簡潔になります。

use std::fs::read_to_string;
use thiserror::Error;

fn main() -> Result<(), MyError> {
    let html = render()?;
    println!("{}", html);
    Ok(())
}

fn render() -> Result<String, MyError> {
    let file = std::env::var("MARKDOWN")?;
    let source = read_to_string(file)?;
    Ok(source)
}

#[derive(Error, Debug)]
enum MyError {
    #[error("Environment variable not found")]
    EnvironmentVariableNotFound(#[from] std::env::VarError),
    #[error(transparent)]
    IOError(#[from] std::io::Error),
}

マクロを使って From 実装を自動化できるので、非常に便利です。

4. anyhow を使用

anyhowBox<dyn Error> のラッパーですが、エラーハンドリングをより簡単にできます。

use std::fs::read_to_string;
use anyhow::Result;

fn main() -> Result<()> {
    let html = render()?;
    println!("{}", html);
    Ok(())
}

fn render() -> Result<String> {
    let file = std::env::var("MARKDOWN")?;
    let source = read_to_string(file)?;
    Ok(source)
}

Just do it

これで Rust のエラー処理が怖くなくなったはずです。上記の方法を活用すれば、実際の開発でエラー処理に困ることはなくなります!


私たちはLeapcell、Rustプロジェクトのホスティングの最適解です。

Leapcell

Leapcellは、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです:

複数言語サポート

  • Node.js、Python、Go、Rustで開発できます。

無制限のプロジェクトデプロイ

  • 使用量に応じて料金を支払い、リクエストがなければ料金は発生しません。

比類のないコスト効率

  • 使用量に応じた支払い、アイドル時間は課金されません。
  • 例: $25で6.94Mリクエスト、平均応答時間60ms。

洗練された開発者体験

  • 直感的なUIで簡単に設定できます。
  • 完全自動化されたCI/CDパイプラインとGitOps統合。
  • 実行可能なインサイトのためのリアルタイムのメトリクスとログ。

簡単なスケーラビリティと高パフォーマンス

  • 高い同時実行性を容易に処理するためのオートスケーリング。
  • ゼロ運用オーバーヘッド — 構築に集中できます。

ドキュメントで詳細を確認!

Try Leapcell

Xでフォローする:@LeapcellHQ


ブログでこの記事を読む

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?