Rubyでは、プログラムの実行中にファイルが見つからない、数値変換に失敗する、外部サービスとの通信に失敗するといった「例外」が発生することがあります。そのような状況を適切に処理するために使われるのが例外処理です。
Rubyの代表的な例外処理では、begin、rescue、ensureを組み合わせます。簡潔に整理すると、beginは例外が発生する可能性のある処理を実行する部分、rescueは発生した例外を捕捉して対応する部分、ensureは例外の有無にかかわらず実行したい後処理を書く部分です。
それぞれの役割を理解すると、単にエラーでプログラムが停止するのを防ぐだけでなく、エラーの種類に応じた処理や確実な後片付けができるようになります。この記事では具体的なコード例とともに使い分けを解説します。
Rubyのbegin・rescue・ensureの基本構造
Rubyで3つを組み合わせる場合、基本的な構造は次のようになります。
begin
# 例外が発生する可能性のある処理
rescue => e
# 例外が発生した場合の処理
ensure
# 例外の有無にかかわらず実行する処理
end
処理はまずbegin側から開始されます。そこで対象となる例外が発生すると、通常の処理を中断して対応するrescueへ移ります。その後、ensureがあれば後処理が実行されます。
一方、begin内で例外が発生しなかった場合はrescueを通りません。それでもensureは実行されます。この違いが3つの役割を理解するうえで重要です。
beginの役割|例外処理の対象となる処理を書く
beginは、例外処理の対象となる一連の処理を開始するために使います。ファイル操作、ネットワーク通信、データ変換など、失敗する可能性がある処理を囲む場面が代表例です。
例えば文字列を整数へ変換するIntegerメソッドでは、整数として解釈できない文字列を渡すとArgumentErrorが発生します。
begin
number = Integer("abc")
puts number
rescue ArgumentError
puts "整数に変換できませんでした"
end
この例ではInteger("abc")がbegin側の処理です。変換に失敗してArgumentErrorが発生すると、以降のputs numberへは進まず、対応するrescueが実行されます。
rescueの役割|発生した例外を捕捉して処理する
rescueは、対象となる例外が発生した場合の対応を書くために使います。エラーメッセージを表示する、ログへ記録する、代替値を設定する、処理をやり直すといった対応が考えられます。
また、例外オブジェクトを変数で受け取ることもできます。
begin
number = Integer("abc")
rescue ArgumentError => e
puts "変換に失敗しました"
puts e.message
end
ここではeに例外オブジェクトが入り、e.messageから例外のメッセージを取得できます。実際のアプリケーションでは、こうした情報をログへ記録して原因調査に利用することもあります。
rescueでは例外の種類を指定できる
Rubyでは、発生する可能性のある例外に応じて複数のrescueを用意できます。これによって「何が失敗したのか」に応じた異なる対応ができます。
begin
# 何らかの処理
rescue ArgumentError => e
puts "引数に問題があります"
rescue IOError => e
puts "入出力処理に問題があります"
end
このように具体的な例外クラスを指定することは、適切な例外処理を書くうえで重要です。何でも同じエラーとして処理してしまうより、想定している失敗だけを捕捉する方が、本来発見すべきプログラム上の問題を見逃しにくくなります。
なお、例外クラスを指定しない通常のrescueは、すべてのExceptionを無条件に捕捉するものではなく、基本的にはStandardErrorとそのサブクラスを対象とします。この点はRubyの例外処理で混同されやすい部分です。
ensureの役割|成功・失敗にかかわらず後処理を行う
ensureは、例外が発生した場合にも発生しなかった場合にも実行したい処理を書くために使います。代表的なのは、利用したリソースの解放や終了処理です。
動きを確認する簡単な例は次のとおりです。
begin
puts "処理を開始します"
Integer("abc")
rescue ArgumentError
puts "例外が発生しました"
ensure
puts "終了処理を行います"
end
この場合はArgumentErrorが発生するため、「処理を開始します」→「例外が発生しました」→「終了処理を行います」という順番になります。
一方、Integer("123")のように正常に変換できる値へ変更するとrescueは実行されませんが、ensureの「終了処理を行います」は実行されます。つまり、ensureは成功時と失敗時のどちらでも必要な処理に向いています。
3つの違いを表で整理
begin、rescue、ensureの違いを整理すると次のようになります。
| キーワード | 主な役割 | 実行されるタイミング |
|---|---|---|
begin |
例外処理の対象となる処理を開始する | 通常の処理として最初に実行 |
rescue |
発生した例外を捕捉して対応する | 対応する例外が発生した場合 |
ensure |
後片付けなどを行う | 原則として例外発生の有無にかかわらず実行 |
覚え方としては、「beginで試す、rescueで失敗に対応する、ensureで最後の後片付けをする」と考えると分かりやすいでしょう。
ただしbeginはJavaなどのtryと完全に同じキーワードというわけではありません。Rubyにはメソッド定義など、明示的なbeginを書かなくてもrescueやensureを記述できる構文もあります。
ファイル操作で考えるとensureの必要性が分かりやすい
ファイルや外部リソースを扱う場面を考えると、ensureの目的を理解しやすくなります。途中で例外が発生しても、開いたリソースを適切に閉じる必要があるからです。
file = nil
begin
file = File.open("sample.txt", "r")
puts file.read
rescue Errno::ENOENT => e
puts "ファイルが見つかりません"
ensure
file.close if file
end
ファイルを正常に読めた場合にも、読み込み途中で問題が起きた場合にも、開くことに成功したファイルは最後に閉じたいところです。そのような「結果にかかわらず必要な後処理」をensureへ置けます。
ただしRubyの実践的なコードでは、ファイル操作ならFile.openへブロックを渡すことで、ブロック終了時に自動的にファイルを閉じてもらう方法が一般的です。
File.open("sample.txt", "r") do |file|
puts file.read
end
つまり、ensureを覚えることは重要ですが、Rubyや利用中のライブラリが安全なリソース管理用APIを提供している場合は、そちらを利用した方が簡潔で安全なコードになることがあります。
elseを組み合わせることもできる
Rubyの例外処理にはbegin、rescue、ensureだけでなく、elseも組み合わせられます。elseはbegin側で例外が発生しなかった場合に実行する処理を書くために利用できます。
begin
number = Integer("123")
rescue ArgumentError
puts "変換に失敗しました"
else
puts "正常に変換できました: #{number}"
ensure
puts "処理を終了します"
end
このコードでは変換に成功するためelseが実行され、最後にensureも実行されます。失敗すればrescueへ進み、その後にensureが実行されます。
そのため、より詳しく覚えるなら「begin=本処理、rescue=例外時、else=正常終了時、ensure=どちらの場合でも必要な後処理」と整理できます。
rescueで何でも握りつぶさないことが重要
例外処理を覚え始めたときに注意したいのが、エラーが出ないようにする目的だけでrescueを使うことです。例外を捕捉して何もしなければ、プログラム上の重要な問題まで見えなくなる可能性があります。
例えば、原因を確認せずすべての例外に対して空の処理を行うと、データ保存に失敗しているのにプログラムだけ正常に進んでいるように見えることがあります。
実際の開発では、想定できる例外クラスをできるだけ具体的に指定し、必要に応じてログを残したり、利用者へ適切なエラーを表示したり、上位の処理へ例外を伝えたりすることが重要です。rescueはエラーを隠す機能ではなく、想定される異常状態へ適切に対応するための仕組みと考えるとよいでしょう。
begin・rescue・ensureの処理順を具体例で理解する
最後に、正常時と例外発生時の流れを比較してみましょう。例外が発生しない場合は、基本的に「begin内の処理→elseがあればelse→ensure」という流れになります。
対象となる例外が発生した場合は、「begin内で例外発生→対応するrescue→ensure」という流れです。例外が発生した地点より後ろにあるbegin内の通常処理は実行されません。
例えば「APIからデータを取得する→データを保存する→終了処理をする」というプログラムで、取得中に想定した通信エラーが発生したとします。rescueで通信エラーを記録し、ensureで必要なリソースを解放する、といった設計が考えられます。このように役割を分離すると、例外が発生したときにも処理の流れを把握しやすくなります。
まとめ|beginは本処理、rescueは例外対応、ensureは必ず行いたい後処理
Rubyの例外処理におけるbegin、rescue、ensureは、それぞれ明確に役割が異なります。beginでは例外が発生する可能性のある処理を実行し、rescueでは発生した例外へ対応し、ensureでは成功・失敗にかかわらず必要な後処理を実行します。
最初は「begin=試す処理」「rescue=エラー時の処理」「ensure=最後に必ず行いたい処理」と覚えておけば十分です。さらに慣れてきたら、例外クラスを具体的に指定する方法、例外オブジェクトの利用、else、メソッド内でのrescueなどを学ぶと、より実践的な例外処理を書けるようになります。
特に重要なのは、単にプログラムを止めないためにrescueするのではなく、「どの失敗を想定し、失敗したときに何をするべきか」を考えることです。適切な例外処理は、Rubyプログラムを安全で保守しやすいものにするための基本的な技術です。


コメント