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

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

EffectiveRubyの項目23「できる限りもっとも対象の狭い例外を処理できるようにしよう」を読んでいるときに、Rubyにおける例外処理の流れと捕捉について気になったためまとめます。
※else節は例外が発生しなかった場合に実行されるため、今回は対象外とします。

## Rubyにおける例外処理の基本(begin~rescue~end)
まずは、Rubyにおける簡単な例外処理について。
サンプルコードでは、適当なクラスとインスタンスメソッドを定義し、0での除算を行うことで例外を発生させ、それを捕捉しています。

```rb
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を追加します。

```rb
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節が実行されます。

```rb
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モジュールのインスタンスメソッドです。

```rb
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は例外が発生した場合、発生した地点から例外が捕捉されるまでスタックをたどり続けます。
今回の場合、呼び出し元に例外捕捉の処理を加えます。

```rb
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を追加します。

```rb
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節は必ず最後に実行される。