Rustのエラーハンドリング

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

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

Result<T, E>

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

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

エラーをStringで返す

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)

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

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 という型 から 、別の型へ変換するための汎用的な方法を提供している。

参考

--

--

Software engineer

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store