プログラムにおいて処理を続行することが不可能なエラーが発生した場合には、例外処理を行います。
例外処理は、本来実行したい処理とは異なる部分ですが、全く記述しなかったり、不適切に記述すると、エラー発生時にシステムが停止してしまったり、必要な情報を残すことができなかったりするなどの重大な事態を招いてしまう恐れがあります。
ですから、実際の開発現場でプログラミングをするには、必須の処理となります。
本記事では、Rubyにおける例外処理についてまとめます。
プログラミング初心者の方の学習や、忘れてしまった方の復習として、参考にしていただければ幸いです。
記載しているプログラムは、Ruby2.7.1を使って動作確認をしています。
Rubyの例外処理
Rubyの例外処理に関連する記述は以下のとおりです。
- begin…rescue…end文:begin内で発生した例外をrescueで捕捉する。
- raise文:プログラマーが意図的に例外処理を発生させる。
- else文:begin内で例外が発生しなかった場合に実行する処理。begin内で例外が発生した場合は処理されない。
- ensure文:例外が発生してもしなくても際にも必ず実行される処理。
- retry文:rescue節内に記述すると、begin内の処理をもう一度実行する。
- rescue修飾子:メソッド定義などの式の後に、beginなしで例外処理を行うことができる。
begin…rescue…end文
例外が発生する可能性がある文をbegin内に記述します。
例外が発生すると、begin内の例外発生以降の処理はスキップされ、rescue節の中が実行されます。
def main begin puts "before exception." raise_test_exception() puts "after exception." rescue => e puts "rescue." end end
上記のメソッドmainでは、raise_test_exceptionというメソッドを呼び出し、そこでは必ず例外が発生するとします。
例外が発生すると、それ以降の処理はスキップして、rescue節の処理が実行されます。
つまり、上記の例では、”before exception.”が表示された後に、”after exception.”は表示されず、rescue節の”rescue.”が表示されます。
rescue文には捕捉するクラスを指定することできます。指定しない場合は、StandardErrorのサブクラスの例外をすべて捕捉します。
複数種類の例外が発生する可能性がある場合には、rescue文に複数指定することができます。
rescue節の処理が異なる場合には、rescue文を複数記述します。
def main begin puts "before exception." raise_test_exception() puts "after exception." rescue IOError => e puts "IOException." rescue IndexError => e puts "IndexError." end end
rescue節の処理が同じである場合には、1つのrescue文に複数の捕捉対象のクラスを指定します。
def main begin puts "before exception." raise_test_exception() puts "after exception." rescue IOError, IndexError => e puts e.message end end
raise文
raise文を使うことで、プログラマーが任意の場所で例外を発生させることができます。
def raise_test_exception puts "before raise." raise "Test exception." puts "after raise." end
上記のメソッドraise_test_exceptionでは、この関数が呼び出されると必ず例外が発生します。
例外が発生すると、それ以降の処理はスキップして、この関数を抜けて呼び出し元の処理に戻ります。
つまり、上記のプログラムでは、”before raise.”が表示された後に、”after raise.”は表示されず、このメソッドを抜けます。
raise文に例外クラスを指定しない場合は、自動的にRuntimeErrorクラスとして例外を送出します。
例外クラスを指定することもできます。
def raise_test_exception puts "before raise." raise IOError, "Test exception." puts "after raise." end
上記のプログラムでは、IOErrorとして例外を作成します。
(上記は記述例のためIOErrorとしましたが、本来は発生する例外の種類に応じて、適切なクラスを選択しなければなりません)
else文
begin…rescue…end文には、else文を指定することができます。
else文を指定すると、begin内で例外が発生しなかった場合に、else節の処理を実行することができます。
def main begin puts "before exception." raise_test_exception() # -> 例外発生せず puts "after exception." rescue => e puts "exception." else puts "not exception." end end
上記の例では、”before exception.”が表示された後に、例外が発生せず、”after exception.”が表示され、その後else節の処理が実行されて、”not exception.” が表示されます。
ensure文
ensure文を使うと、例外が発生しても、発生しなくても、どちらの場合でも必ず処理を実行させることができます。
def main begin puts "before exception." raise_test_exception() puts "after exception." rescue => e puts "exception." else puts "not exception." ensure puts "ensure." end end
上記の例では、例外が発生しない場合は、”before exception.” → “after exception.” → “not exceptin.” → “ensure.”の順に表示され、begin、elseの後に、ensureの処理が実行されます。
例外が発生した場合は、”before exception.”→”ecxeption.” → “ensure.”の順に表示され、begin、rescueの後に、ensureの処理が実行されます。
ensureでは、通常例外が発生しても、しなくても必ず必要となる、リソース開放などの事後処理を記述することが多いです。
プログラムの構成によっては、rescue文は記述せずに、begin…ensure文だけ記述する場合もあります。
retry文
retry文をrescue節内に記述すると、begin内の処理をもう一度実行します。
その名の通り、リトライ処理を実施したい場合に使用します。
相手の状態によって、リトライすることで上手くいく可能性があれば使用する価値がありますが、ファイルの権限エラーなど、リトライしても再度例外が発生するのが分かっている場合は、無限ループになりますので注意が必要です。(使用する場合は、基本的にリトライ条件を自分で実装する必要があります)
def main retry_count = 0 begin puts "before exception." raise_test_exception() puts "after exception." rescue => e puts "exception." retry_count += 1 retry if retry_count <= 3 # -> リトライは最大3回実施 end end
rescue修飾子
メソッド定義などの式の後に、beginなしで例外処理を行うことができます。
例えば以下のプログラムでは、必ずZeroDivisionErrorが発生しますが、rescueによって変数aには0が格納され、putsで0が表示されます。
a = 1 / 0 rescue 0 puts a # -> 0
エラーオブジェクト
rescue文では、例外情報として例外オブジェクトを補足することができます。
def main begin puts "before exception." raise_test_exception() puts "after exception." rescue => e puts e.message end end
上記の例では、eという変数に補足したオブジェクトを格納し、コンソールにeが持つメッセージを表示しています。
rescue文で指定したクラスの例外を捕捉しますが、いずれにも該当しない例外クラスの場合は、上位にraiseされます。
逆に、自分でraiseする場合には、任意の型のオブジェクトを例外情報として指定することができます。
def raise_test_exception puts "before raise." raise StandardError, "Test exception." puts "after raise." end
上記の例では、Rubyが標準で提供している、StandardErrorクラスを使用しています。
エラーの種類を区別するために、Rubyでは標準でいくつかの例外オブジェクトを提供しています。
下記はその一例です。
- IOError:入出力でエラーが発生した場合
- IndexError:インデックスに指定した値が範囲外である場合
- RegexpError:正規表現のコンパイルに失敗した場合
- RuntimeError:特定の例外クラスには該当しないエラーが発生した場合。raiseで例外クラスを指定しない場合。
これらを自分でraiseする時に、指定することもできます。
適切なエラーの種類がない場合は、独自の例外オブジェクトを作成することも可能です。
開発現場によっては、独自の例外オブジェクトを用意していて、発生したケースに応じて使い分けたりします。
独自の例外オブジェクトを作成する際には、基本的にStandardErrorクラスを継承して作成することが多いです。
ログ出力
例外が発生した際に、後からなぜ発生したのかを調査する際に重要となるのがログです。
開発中はデバッグなどにより原因を特定することができますが、本番運用が始まるとデバッグなどはできないので、ログだけが頼りになります。
原因を特定するためには、原因が分かるようなログを出力する必要があります。
その1つがバックトレースです。
バックトレースは、プログラムを処理してきた順序(関数の呼び出し階層)が記録されていて、どこで例外が発生したのかを特定するために必要な情報となります。
Rubyでバックトレースを出力させるには、例外オブジェクトのbacktraceを呼び出します。
def main begin puts "before exception." raise_test_exception() puts "after exception." rescue => e puts e.backtrace end end
上記のプログラムでは、rescue節で例外オブジェクトのbacktraceを呼び出してスタックトレースを表示しています。
バックトレースには以下のような情報が表示され、どのプログラムの何行目で例外が発生したのかを特定することができます。
exception_ruby.rb:21:in `raiseTestException' exception_ruby.rb:5:in `main' exception_ruby.rb:25:in `<main>'
例外発生時のログ出力には、基本的にこのバックトレースとともに、他にデータを特定できる情報(レコードのIDなど)や引数の情報(不正なデータが渡されていないか)などを出力することが多いかと思います。
どんな情報をログ出力すべきかは、経験によるところもあるのですが、自分が調査する立場ならどんな情報が必要となりそうかを考えてみると良いと思います。
例外処理は障害発生の最後の砦
プログラムは必ずと言ってよいほどエラーが発生します。
プログラム自体が完璧であったとしても、連携しているデータベースやネットワーク、OSなどあらゆるところで想定しないエラーが発生します。
プログラマーとしては、あらゆる事態を想定し、エラーが発生してもなるべく影響を少なくし、原因調査のために必要な情報を残すという点を心がける必要があります。
僕個人としては、正常系のプログラムであれば誰でも書けるが、この例外処理を無駄なく適切に書けるプログラマーが良いプログラマーであると思っています。
今回は、Rubyの例外処理についてまとめました。
参考になれば幸いです。
コメント