Edited at

例外をチェインした際に以前の例外を取得する

More than 5 years have passed since last update.

例外が発生した際に、rescue 句 や ensure 句で新しい例外を発生させる場合があります。

begin

raise "Error A"
rescue
raise "Error B"
end

呼び出し元によりわかりやすいエラーメッセージを伝えるために、

独自の例外を作って投げ直したりする場合です。

具体的には以下の様なコードです。

class OreOreError < StandardError; end

def divide_by_zero(num)
num / 0
end

def calculate(num)
divide_by_zero(num) # raise ZeroDivisionError
rescue => e
raise OreOreError.new("#{e.class} が「#{e.message}」って言ってるわ!")
end

begin
calculate(100)
rescue => e
p e #=> #<OreOreError: ZeroDivisionError が「divided by 0」って言ってるわ!>
end

ただこの場合、問題があります。

以前に発生した例外 (上記のコードの場合は ZeroDivisionError) の情報が消えてしまうということです。

例えば、もとの例外の Exception#backtrace が分からなくなるため、

デバッグに支障をきたすこともあるでしょう。

しかし、ここで朗報です!

Ruby 2.1 で追加された Exception#cause を使えば、以前に発生した例外もたどれるようになりました。

begin

calculate(100)
rescue => e
puts "現在のエラー: #{e.inspect}"
puts "1つ前のエラー: #{e.cause.inspect}"
end

# 現在のエラー: #<OreOreError: ZeroDivisionError が「divided by 0」って言ってるわ!>
# 1つ前のエラー: #<ZeroDivisionError: divided by 0>

全ての例外をたどって backtrace を出せるようにしてみたり。

class Exception

def full_backtrace
return [self.inspect] + backtrace unless cause

[self.inspect] + backtrace + cause.full_backtrace
end
end

puts e.full_backtrace * "\n"

# #<OreOreError: ZeroDivisionError が「divided by 0」って言ってるわ!>
# oreore.rb:22:in `rescue in calculate'
# oreore:19:in `calculate'
# oreore:26:in `<main>'
# #<ZeroDivisionError: divided by 0>
# oreore.rb:15:in `/'
# oreore.rb:15:in `divide_by_zero'
# oreore.rb:19:in `calculate'
# oreore.rb:26:in `<main>'


Ruby 2.0 以前の場合

Ruby 2.0 以前の場合は Exception#cause が使えないため、少し工夫が必要です。

例外オブジェクトのインスタンス変数に、以前の例外を持たせるようにします。

$! は Ruby の組み込み変数 (Kernel の特殊変数) の1つで、

最後に例外が発生したときの Exception オブジェクトを持っています。

class OreOreError < StandardError

attr_reader :previous # Exception#cause の代わり

def initialize(msg)
super(msg)
@previous = $!
end
end

def divide_by_zero(num)
num / 0
end

def calculate(num)
divide_by_zero(num) # raise ZeroDivisionError
rescue => e
raise OreOreError.new("#{e.class} が「#{e.message}」って言ってるわ!")
end

begin
calculate(100)
rescue => e
puts "現在のエラー: #{e.inspect}"
puts "1つ前のエラー: #{e.previous.inspect}"
end

# 現在のエラー: #<OreOreError: ZeroDivisionError が「divided by 0」って言ってるわ!>
# 1つ前のエラー: #<ZeroDivisionError: divided by 0>


参考