エラー処理の方法

仕事で他人(といっても先輩だけど…)の書いたコードを書き直すことがよくあるんだけど、
種類の違うエラーが全て1つのフラグ変数によって処理されてて、非常に読みにくかった。
雰囲気はこんな感じ

# 汎用のエラー変数(変数名の意味は謎...)
my $iErrFlg = 0;
my $Errmsg  = q{};

# ...

if ( !$dir_name = getTodaysdir() ) {
    $iErrFlg = 1;
    $Errmsg .= "Failed to make dir for today.\n";
    # ここで抜けずに、処理はつづく... (つまり、最後までどんなエラー処理が行なわれるかわらない)
}

# ソースが続く ...(しかも途中で、エラー判断のたびに $iErrFlg が使いまわされてる)

# やっと最後の段階になってエラー処理
if ($iErrFlg) {
    sendErrMessage($from, $to, $Errmsg);
    exit 1;
}


直すのがめんどうに感じたので、このエラー処理構造は変えずにコードを編集していったら、さらに読みにくくなって、とてもじゃないけど、異常系の処理の流れが追えなくなってきた。
問題点は、

  • エラーがその場で処理されないので、最後までソースを読まないとどう対処しているか分からない
  • エラーがその場で処理されないので、エラーの処理を忘れる恐れがある
  • そもそもエラーフラグを使いまわしているので、コードの位置によってフラグの意味が変わってる
    • 「何かエラーが起きた」という意味は当然変わってないが、「何で起きたエラーか?」という意味が変わる
    • このフラグでは「エラーが起きたかどうか」しか分からない。よってエラーの原因を探るにはコードを追う必要がある


反省して、「Perl Best Practice」を読んでみたら、まさに求めていた処理方法が詳しく載っていた。
ポイントは、何かがうまくいかなくなったら

「特別なエラー値を返す」

のではなく、

「例外を送出する」

というところ。


ファイルを開くだけの簡単な関数 file_open() を例にすると、以下のようになる。

use Carp;

for my $file_name (@ARGV) {
    # 明示的にエラーを補足(補足しなければ例外が投げられてこの場で終了する)
    if (my $fh = eval { file_open($file_name) }) {
        print "Open SUCCEEDED for file ($file_name)\n";
        # $fh を使ってごにょごにょ
    }   
    else {
        print "Open FAILED for file  ($file_name)\n";
        # 失敗した場合の処理が続く(メールを送って終了するとか)
    }   
}

sub file_open {
    my ($file_name) = @_; 
    if (-r $file_name) {
        open my $fh, $file_name
            or croak "Readable ($file_name), but could not open ($file_name)";
        return $fh;
    }   

    croak "Not even readable ($file_name)";
}


これだったら、エラー処理のし忘れの心配もない。
また、例外を無視(補足)するためには、明示的に eval を使う必要があるので、「補足されないエラー」に注意を払うことができる。


明日会社に行ったら早速実践してみよ。