Rustのエラーハンドリング

Takanori Ishibashi
8 min readMay 2, 2018

個人で試しに書いているプログラムだと、エラーハンドリングを丁寧に行わない場合がある。しかし仕事で書いているプログラムだと、エラーが発生した場合、他の人が読んでも理解しやすいようにエラーの内容や原因を丁寧に出力したり、エラーの内容によってリトライさせる必要がある。 そのために最低限必要であろうエラーハンドリングの方法をまとめた。

まず、Rustの例外にはpanic!とResult<T, E> がある。panic!は発生したエラーが回復不可能な場合や契約違反が発生した場合などに使用する。panic!はプログラムをアボードするので、原則として捕獲されない。catch_unwind(おそらくLLVMの例外機構のレイヤーでの巻き戻し)を使用して捕獲できる場合もあるが、推奨はされていない。
関数やメソッドの呼び出し元でエラーをどう扱うかを判断させる場合は、Result<T, E>を返すことになる。関数やメソッドの呼び出し元はResult<T, E> の値を取り出して内容を判断することになる。呼び出し元が毎回判断するのは面倒なように思う人もいるかもしれないが、大域脱出的なことが発生しない分、素直にプログラムを読み下していけるも言える。(呼び出し元が都度エラーハンドリングすることは賛否両論あると思われる)

Result<T, E>

Result<T, E> 型は エラーになる可能性を示す列挙型で、処理が成功した場合の値はOk、処理が失敗した場合の値はErrで包み込む。

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

Result<T, E>から値を取り出す方法は用途ごとに数多くあり、Enum std::result::Result で確認できる。

エラーをStringで返す

1つの関数の中で型の異なるエラーが発生する可能性がある場合、エラーの型をまとめたくなる。短いプログラムであれば、Result<T, E>のEをStringにして返す手段が考えられる。
以下のコードは複数の異なる型のエラーをまとめてStringに変換し、関数の呼び出し元にエラーの情報を伝えている。ただし、どのエラーもStringで返すため、エラーが発生したもともとの型(std::num::ParseIntError、もしくはstd::io::Error)の情報は失われてしまい、関数の呼び出し元で型情報による処理の分岐ができないという問題がある。

use std::fs::File;
use std::io::Read;
use std::path::Path;
fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, String> {
let mut file = File::open(file_path).map_err(|e| e.to_string())?;
let mut contents = String::new();
file.read_to_string(&mut contents)
.map_err(|e| e.to_string())?;
let n: i32 = contents.trim().parse::<i32>().map_err(|e| e.to_string())?;
Ok(2 * n)
}
fn main() {
match file_double("foobar") {
Ok(n) => println!("{}", n),
Err(err) => println!("Error: {:?}", err),
}
}

なお、以下のバージョンのRustで動作確認している。

rustc 1.27.0-nightly (79252ff4e 2018–04–29)

独自のエラー型を定義する

この節では、独自のエラーの型を定義することで、上の”エラーをStringで返す”に書いた問題を解決しようとしてる。 以下のコードはNetwork Programming with Rustで公開されているコードを使用している。各ライブラリのバージョンはCargo.tomlから確認できる。error.rs というファイルにエラーに関する処理がまとめている。

use std::error::Error;
use std::convert::From;
use std::fmt;
use diesel::result::Error as DieselError;
use rocket::http::Status;
use rocket::response::{Response, Responder};
use rocket::Request;
#[derive(Debug)]
pub enum ApiError {
NotFound,
InternalServerError,
}
impl fmt::Display for ApiError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
ApiError::NotFound => f.write_str("NotFound"),
ApiError::InternalServerError => f.write_str("InternalServerError"),
}
}
}
impl From<DieselError> for ApiError {
fn from(e: DieselError) -> Self {
match e {
DieselError::NotFound => ApiError::NotFound,
_ => ApiError::InternalServerError,
}
}
}
impl Error for ApiError {
fn description(&self) -> &str {
match *self {
ApiError::NotFound => "Record not found",
ApiError::InternalServerError => "Internal server error",
}
}
}
impl<'r> Responder<'r> for ApiError {
fn respond_to(self, _request: &Request) -> Result<Response<'r>, Status> {
match self {
ApiError::NotFound => Err(Status::NotFound),
_ => Err(Status::InternalServerError),
}
}
}

ここでは以下の3点が重要である。上から順に説明していく。

  1. 独自のエラー型を定義
  2. Errorトレイトを実装
  3. Fromトレイトを実装

独自のエラー型を定義

pub enum ApiError {
NotFound,
InternalServerError,
}

enumを使用し独自のエラー型を定義している。関連のあるエラーをまとめておくことで、呼び出し側の戻り値をのResult<T, E>のEにApiErrorを書くことができる。

Errorトレイトを実装

impl Error for ApiError {
fn description(&self) -> &str {
match *self {
ApiError::NotFound => "Record not found",
ApiError::InternalServerError => "Internal server error",
}
}
}

descriptionにエラーの簡単な説明を実装してる。またエラーが発生した lower-levelの原因をcauseに任意で実装できる。

Fromトレイトを実装

impl From<DieselError> for ApiError {
fn from(e: DieselError) -> Self {
match e {
DieselError::NotFound => ApiError::NotFound,
_ => ApiError::InternalServerError,
}
}
}

diesel(ここで使用されているORM)のエラーを独自に定義したエラー型に変換している。Fromは、ある特定の T という型 から 、別の型へ変換するための汎用的な方法を提供している。

参考

--

--