Rubyでカスタム例外を作る時にStandardErrorを継承するのはなぜか?
背景
Rubyでカスタム例外を作るにあたりWeb検索したところ「カスタム例外を作る時はStandardErrorを継承する」と書かれた記事を多く目にしました。
そうなんだーと思いつつも、その理由がよくわからなかったので、調べてみました。
結論
- rescueに引数を渡さない場合、StandardErrorを継承した例外クラスの捕捉をするから
- RubyにおけるStandardErrorは、アプリケーションレベルで最上位の例外クラスだから
- StandardErrorの上位であるExceptionクラスを継承すると、システムレベルの例外も捕捉するから
なぜStandardErrorを継承するのか?
ここでいうrescueは例外処理で扱うrescue節のことです。
rescueは「第一引数に渡した例外クラス以下の例外だけを捕捉し、引数を渡さない場合はStandardError以下を捕捉対象にする」 という仕様になっています。
begin # 例外が起きうる処理 # rescue StandardError を略している rescue # 例外処理 end
仮にカスタム例外を作る時にStandardError(とそのサブクラス)以外を継承する場合、引数なしのrescueでは捕捉されなくなってしまいます。
なぜStandardErrorを継承するのか?
まずはRubyにおける例外クラスの概念整理をしました。 以下の「例外クラス」に例外クラスがまとまっており、StandardErrorの説明には以下のように書かれています。
通常のプログラムで発生する可能性の高い例外クラスを束ねるためのクラスです。
cf.library _builtin (Ruby 2.7.0 リファレンスマニュアル)
またStandardErrorと並列概念のエラーを以下に抜粋しました。
Exception NoMemoryError // メモリ不足 ScriptError // Rubyスクリプト自体にバグがある LoadError NotImplementedError SyntaxError SecurityError // セキュリティ用の問題が起きた SignalException // 捕捉していないシグナルを受け取った Interrupt StandardError // 通常のプログラムで動作する可能性が高いエラーを束ねるクラス
並列概念にある例外クラスでは、メモリ不足やセキュリティなど アプリケーションではなくシステムよりのエラーを扱っています。
作ろうとしているカスタム例外はアプリケーションレベルと思うので、StandardErrorを継承するのが適切ということですね。
なぜExceptionクラスを継承してはいけないのか?
先述のようにExceptionクラスには、システムエラーも含まれます。 Exceptionクラスを継承したカスタム例外を捕捉するため以下のように書くと思われますが、システムエラーによる例外も捕捉されてしまいます。
begin # 例外が起きうる処理 rescue CustomException # システムエラーも例外処理されてしまう!! # 例外処理 end
これでは適切な例外処理ができないため避けたほうがいい、ということですね。
なお、Rubyスタイルガイドにも以下のように書かれていました。
Avoid rescuing the Exception class. This will trap signals and calls to exit, requiring you to kill -9 the process.
Exceptionクラスをrescueするとシグナルがトラップされてexitが呼ばれるので、プロセスをkill -9で止めなければならなくなります。
cf. Rubyスタイルガイドを読む: 例外処理|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社
感想と展望
今回調べる中で、
- rescueではStandardErrorがdefaultの捕捉対象であること
- StandardErrorを含めたRubyにおける例外クラスの概念図
がわかりました。
(改めて該当箇所を読み返すと、図も含めて解説されていたこともわかりました...) amzn.to
調査の中でエラーハンドリングに関する記事もいくつか見つけたので、RubyやRailsのエラーハンドリングに関するベスト/バッドプラクティスも近々整理したいなあ。