Rubyの例外処理をマスターする:実践ガイド
AppSignalでは、Rubyアプリケーション向けのエラー追跡サービスを提供しています。アプリケーションが発生させたすべての例外をキャプチャし、発生した瞬間に開発者へ通知します。
例外処理を正しく実装するのは、実はかなり難しいものです。この記事では、Rubyの例外処理の仕組み、不適切なハンドリングが引き起こす問題、そして例外を適切にrescue(捕捉)する方法について詳しく解説します。
例外をrescueする基本
Rubyで例外をrescueすると、エラーが発生した瞬間にアプリケーションがクラッシュするのを防げます。begin .. rescueブロックを使えば、エラー発生時にアプリケーションが取るべき代替パスを指定できます。
どの例外をrescueするかを指定することも可能です。例外クラスを指定すると、そのサブクラスもすべて捕捉されます。
上記の例では、親クラスであるSystemCallErrorをrescueすると、子クラスのErrno::ENOENTも一緒に捕捉されることがわかります。
例外階層の上位をrescueしすぎる問題
例外階層のあまりに上位のクラスをrescueしないことが重要です。上位のクラスをrescueすると、そのすべてのサブクラスも一緒に捕捉されてしまい、rescueブロックの捕捉範囲が広すぎて汎用的になります。
次のコードは、コマンドライン引数で指定された設定ファイルを読み込むプログラムの例です。
エラーメッセージには「設定ファイルを読み込めなかった」と表示されますが、本当の原因はコード内のタイプミスでした。
begin .. rescueブロックがデフォルトで捕捉する例外クラスはStandardErrorです。具体的なクラスを指定しない場合、RubyはStandardErrorとそのすべてのサブクラスをrescueします。NoMethodErrorもその一つです。
特定の例外クラスをrescueすることで、無関係なエラーが意図せず失敗状態を引き起こすのを防げます。さらに、エンドユーザーにとってより役立つ、具体的なカスタムエラーメッセージを表示できるようにもなります。
Exceptionをrescueすることの危険性
それでも、例外階層のできるだけ上位をrescueしたくなるかもしれません。アプリケーションが発生しうるすべてのエラーを捕捉すれば、クラッシュを防げますから(100%の稼働率だ!)。しかし、これは多くの問題を引き起こします。
Exceptionクラスは、Rubyにおける例外の最上位クラスです。他のすべての例外はこのクラスのサブクラスであり、Exceptionをrescueするとすべてのエラーが捕捉されてしまいます。
ほとんどのアプリケーションがrescueすべきでない代表的な例外として、SignalExceptionとSystemExitが挙げられます。
SignalExceptionは、外部からアプリケーションへ停止が指示されたときに発生します。例えば、OSがシャットダウンしようとしている場合や、システム管理者がアプリケーションを停止したい場合などです。
SystemExitは、Rubyアプリケーションからexitが呼び出されたときに発生します。この例外がraiseされるとき、開発者にはアプリケーションを停止させる明確な意図があります。
Exceptionをrescueしていると、これらの例外がbegin ... rescue ... endブロックの実行中に発生しても、アプリケーションは正常に終了できなくなります。
通常の状況でExceptionをrescueするのは避けるべきです。rescueしてしまうと、SignalExceptionやSystemExitだけでなく、LoadError、SyntaxError、NoMemoryErrorなども正しく機能しなくなります。より具体的な例外をrescueする方が賢明です。
テストの失敗が隠れてしまう問題
rescue Exception => eのようにExceptionをrescueすると、アプリケーション本体以外の部分まで壊れてしまうことがあります。テストスイートがエラーを隠してしまう可能性もあるのです。
minitestやRSpecでは、アサーション(検証)が失敗すると例外がraiseされ、テストが失敗したことが通知されます。このとき、これらのフレームワークは独自のカスタム例外(Exceptionのサブクラス)を発生させています。
そのため、テストコードやアプリケーションコード内でExceptionをrescueすると、アサーションの失敗が握りつぶされる恐れがあるのです。
例外の発生を期待するテストの書き方
中には、例外を発生させることを前提としたコードもあります。テストスイートでは、例外を単純に握りつぶすことで、例外が発生してもテストが失敗しないようにできます。
しかしこの書き方では、例外が実際に発生したかどうかを検証できません。もし例外が発生しなかった場合でも、テストは動作が正しいかどうかを判断できないのです。
そこで、期待した例外が発生したことをアサートし、発生しなかった場合には代わりにどの例外が発生したのかを確認できるようにしましょう。
例外の再送出(re-raise)
アプリケーションがExceptionクラスレベルで例外を捕捉すべきなのは、ごく限られた正当な理由がある場合だけです。例えば、コードブロックを抜ける前に、削除が必須の一時ファイルを消すといったクリーンアップ処理が必要なケースです。
どうしてもExceptionをrescueしなければならない場合は、エラー処理を終えた後に例外を再送出(re-raise)することをお勧めします。こうすることで、その後のプロセスの挙動をRuby本来の例外処理機構に委ねられます。
何をrescueすればいいかわからないとき
繰り返しになりますが、どのエラーをrescueするかは具体的に指定するのが良いプラクティスです。
ある処理がどんな例外を発生させるかわからない場合は、まずStandardErrorをrescueするところから始めるのが良いでしょう。さまざまなシナリオでコードを実行し、実際にどんな例外が発生するのかを観察してください。
新しい例外に遭遇するたびに、その例外自身または適切な親クラスに対する個別のrescueケースを追加していきましょう。多くの例外をまとめてrescueするよりも、対象を絞ってrescueする方がずっと安全です。
以上で、Rubyの例外処理に関する入門編は終わりです。さらに詳しく知りたいことや具体的な質問がある場合は、@AppSignalまでお気軽にお知らせください。また、アプリケーション内で例外がどこで、どのくらいの頻度で発生しているかを可視化したい方は、ぜひAppSignalをお試しください。
著者:Tom de Bruijn
-
RubyでAWS Lambda関数を書いてみよう!初心者向け実践ガイド
AWS Lambdaを使えば、オーバーヘッドを最小限に抑えながらスケーラブルな関数を構築できます。Railsアプリ全体を作成・ホスト・保守する代わりに、Lambda関数を活用すれば、個々のイベントに対して独立して応答する仕組みを簡単に実現できます。本記事では、AWS初心者のあなたを、自分自身のRuby製Lambda関数を書けるレベルまで導きます。 Lambdaは、サーバーを管理することなく、イベントに反応してコードを実行できるサービスです。このイベント駆動型アーキテクチャにより、コードが実際に稼働している時間にのみ課金され、アイドル状態の間は費用が発生しません。LambdaはS3バケットから
-
Rubyで例外にコンテキストデータを追加する3つの方法
標準のバックトレースやエラーメッセージだけでは、エラーの原因を特定するのに十分な情報が得られないことがあります。そんなときに役立つのが、例外への追加データの付与です。幸い、Rubyではこれをとても簡単に実現できます。 エラーメッセージをカスタマイズする 例外にコンテキスト情報を加える最もシンプルな方法は、メッセージそのものに情報を組み込んでしまうことです。次の例では、発生した例外を捕捉し、新しいメッセージを付けて再送出しています。 begin raise foo rescue => e raise e.class, bar end # RuntimeError: bar