言語側で実装されていれば、なかば強制的に使うことになりますし、そして正常系の処理と異常時の処理をスッキリ分離できるので便利な例外処理ですが、開発の過程では逆に厄介な現象を招くこともあります。
例外処理の基本
C++やJavaScriptなどでは、例外としてどんなものでも投げることができますが、RubyやJavaでは、特定の基底クラスから継承したものしか例外として投げることはできません。
そして、例外クラスはたいていの場合、継承ツリーを通じていくつかに区分されています。Javaであれば「処理系自体の異常(Error
)」、「チェック例外(Exception
)」、「非チェック例外(RuntimeException
)」、Rubyでは「通常実行時に起きる例外(StandardError
)」、「それ以外の例外(Exception
)」となっています。そして、Javaでは検査例外がありますが、Rubyでは何も明示しなければStandardError
だけを拾う、ということになっています。
ハマったパターン
Rubyでプログラムを書いていて、ちょうどRailsのsave
とsave!
のように、例外を返すバージョンと返さないバージョンの関数を作り分けていました。
def get!(*args)
# 異常時には例外を起こす
raise ArgumentError unless hoge
# 各種の処理
piyo
end
def get(*args)
get!(*args) rescue nil
end
このようにして書き進めていたところ、get
の返り値が(args
を考えれば何かしら返すはずなのに)nil
になってしまう、ということが起きました。原因を追いかけてみると、get!
の内部でハッシュの添字に指定する値を間違えたために取得した値がnil
になっていて、それにメソッドを呼び出した結果がNoMethodError
となっていました。で、NoMethodError
はStandardError
のサブクラスなので、get
内で**rescue
されて**いて、外にも例外が見えなくなってしまっていたのでした。
結局、get!
内のバグを修正したのはもちろんですが、rescue
も内部で意図的に生成しているArgumentError
だけ拾うようにして、今後似たような問題が起きるのを予防することにしました。
Rubyでは、呼び出した先にメソッドがないNoMethodError
や、ローカル変数がないときのNameError
といった例外もStandardError
のサブクラスなので、何も書かないrescue
でキャッチされてしまいます。
教訓
起きる例外がわかっている場面では、必要最小限の例外だけキャッチしましょう。Rubyでは、プログラムミスで起きる例外でもrescue
されてしまうことがあります。
また、メモリ不足や処理系内部のエラーなどは、自分で対処する方法がないのであればキャッチしてはいけませんし、シンタックスエラーやファイルロードの失敗といった、基本的にプログラムミスでしか起きないようなものは、irb
やpry
のような、ユーザー入力をプログラムにするようなものを作っているのでなければ、キャッチするよりプログラム側、gem導入など処理系側の修正が必要な場面でしょう。