Ruby

begin~rescue~ensureとraiseを利用した例外処理の流れと捕捉について

More than 1 year has passed since last update.


begin~rescue~ensureとraiseを利用した例外処理の流れと捕捉について

EffectiveRubyの項目23「できる限りもっとも対象の狭い例外を処理できるようにしよう」を読んでいるときに、Rubyにおける例外処理の流れと捕捉について気になったためまとめます。

※else節は例外が発生しなかった場合に実行されるため、今回は対象外とします。


Rubyにおける例外処理の基本(begin~rescue~end)

まずは、Rubyにおける簡単な例外処理について。

サンプルコードでは、適当なクラスとインスタンスメソッドを定義し、0での除算を行うことで例外を発生させ、それを捕捉しています。

class ExceptionTest

def test
begin
# 0での除算でエラーを発生させる
1/0
rescue ZeroDivisionError => ex
puts "ZeroDivisionError"
end
end
end

obj = ExceptionTest.new
# 例外発生
obj.test # => ZeroDivisionError

問題なく例外が捕捉されています。

今回は捕捉する例外の範囲を狭めており、ZeroDivisionErrorのみの捕捉を行っています。

ZeroDivisionErrorはStandardErrorのサブクラスになります。


rescue StandardError => exを追加

次に、先ほどのコードにZeroDivisionErrorクラスの親クラスであるStandardErrorを追加します。

class ExceptionTest

def test
begin
# 0での除算でエラーを発生させる
1/0
rescue StandardError => ex
puts "StandardError"
rescue ZeroDivisionError => ex
puts "ZeroDivisionError"
end
end
end

obj = ExceptionTest.new
# 例外発生
obj.test # => StandardError

プロンプトには、「StandardError」が表示されます。

Rubyでは上から下に向かい処理が実行されるため、発生した例外(今回ならZeroDivisionError)の親クラスであるStandardErrorクラスのrescue節が例外を捕捉し、処理を行った後終了しています。

なお、捕捉の順序を逆にするとZeroDivisionErrorのrescue節が実行されます。

class ExceptionTest

def test
begin
# 0での除算でエラーを発生させる
1/0
rescue ZeroDivisionError => ex
puts "ZeroDivisionError"
rescue StandardError => ex
puts "StandardError"
end
end
end

obj = ExceptionTest.new
# 例外発生
obj.test # => ZeroDivisionError


raiseの追加

次に、先ほどのコードにraiseを追加します。

raiseは例外を発生させるKernelモジュールのインスタンスメソッドです。

class ExceptionTest

def test
begin
# 0での除算でエラーを発生させる
1/0
rescue ZeroDivisionError => ex
puts "ZeroDivisionError"
raise
rescue StandardError => ex
puts "StandardError"
end
end
end

obj = ExceptionTest.new
# 例外発生
obj.test

# Error内容
=begin
ZeroDivisionError
exception_test5.rb:6:in `/': divided by 0 (ZeroDivisionError)
from exception_test5.rb:6:in `test'
from exception_test5.rb:18:in `<main>'
=end

Error内容からわかるとおり、raiseが発生した時点で実行中のrescue節が終了し、呼び出し元に戻っています。

Rubyは例外が発生した場合、発生した地点から例外が捕捉されるまでスタックをたどり続けます。

今回の場合、呼び出し元に例外捕捉の処理を加えます。

class ExceptionTest

def test
begin
# 0での除算でエラーを発生させる
1/0
rescue ZeroDivisionError => ex
puts "ZeroDivisionError"
raise
rescue StandardError => ex
puts "StandardError"
end
end
end

obj = ExceptionTest.new
# 例外発生
begin
obj.test
rescue => ex
puts "Other"
puts "class: #{ex.class}"
end

=begin
ZeroDivisionError
Other
class: ZeroDivisionError
=end

しっかりとスタックをたどり、例外が捕捉されていることがわかります。

rescue節内で実行されるraiseメソッドに引数を省略した場合、対象のrescue節の引数と同じオブジェクトが渡されるようです。


ensureを追加

最後に例外の発生にかかわらず実行されるensureを追加します。

class ExceptionTest

def test
begin
# 0での除算でエラーを発生させる
1/0
rescue StandardError => ex
puts "StandardError"
raise
rescue ZeroDivisionError => ex
puts "ZeroDivisionError"
ensure
puts "Ensure"
end
end
end

obj = ExceptionTest.new

begin
obj.test
rescue => ex
puts "Other"
puts "class: #{ex.class}"
end

=begin
StandardError
Ensure
Other
class: ZeroDivisionError
=end

ensure節を追加したことにより処理が行われていますが、raiseにより発生した例外捕捉の前にensure節が実行されています

例外が発生した時点で、呼び出し元に戻るというわけではないようです。


まとめ


  1. 例外処理の実行順序は、begin->rescue(or else)->ensure

  2. 捕捉する例外の範囲(例外クラス)はなるべく狭める。

  3. raiseによる例外発生は、実行中のrescure節を抜ける。


    • 例外を発生させたからといって、その場で処理が終わるわけではない。



  4. Rubyにおける例外は例外が捕捉されるまでスタックをたどる。

  5. ensure節は必ず最後に実行される。