Ruby2.1で導入されたException#causeを参照すれば、例外をrescueして、そこから新たな例外を投げた時も、元の例外のraiseまでたどれます。
MAX_BACKTRACE_DEPTH = 5
def level1
begin
level2
rescue
raise RuntimeError.new("level1で発生したエラーです")
end
end
def level2
begin
level3
rescue
raise RuntimeError.new("level2で発生したエラーです")
end
end
def level3
raise RuntimeError.new("level3で発生したエラーです")
end
#
# 再帰呼び出しでcauseをたどり、すべてのバックトレースを得る
#
def full_backtrace(e, depth:)
items = []
items << e.class.name + " " + e.message
items += e.backtrace
if (e.cause == nil) || (depth >= MAX_BACKTRACE_DEPTH)
return items
else
return items + full_backtrace(e.cause, depth: depth + 1)
end
end
begin
level1
rescue => e
puts full_backtrace(e, depth: 1).join("\n")
end
上記のコードは、このような流れになります。
- level3例外raise
- level2で、level3の例外をrescueして、新たな例外をraise
- level1で、level2の例外をrescueして、新たな例外をraise
出力結果
RuntimeError level1で発生したエラーです
error2.rb:7:in `rescue in level1'
error2.rb:4:in `level1'
error2.rb:35:in `<main>'
RuntimeError level2で発生したエラーです
error2.rb:15:in `rescue in level2'
error2.rb:12:in `level2'
error2.rb:5:in `level1'
error2.rb:35:in `<main>'
RuntimeError level3で発生したエラーです
error2.rb:20:in `level3'
error2.rb:13:in `level2'
error2.rb:5:in `level1'
error2.rb:35:in `<main>'
level1 -> level2 -> level3と、最初にlevel3の例外をraiseした場所まで追えます。エラーハンドラでこのようにログ出力しておくと、エラーの原因を特定しやすくなりますね。
ちなみに、MAX_BACKTRACE_DEPTHを2に設定すると、level2でraiseした例外までしか出力しません。実際の開発でも例外を追う階層に制限をかけたほうがいいかもしれません。
RuntimeError level1で発生したエラーです
error2.rb:7:in `rescue in level1'
error2.rb:4:in `level1'
error2.rb:35:in `<main>'
RuntimeError level2で発生したエラーです
error2.rb:15:in `rescue in level2'
error2.rb:12:in `level2'
error2.rb:5:in `level1'
error2.rb:35:in `<main>'
さらに余談ですが、
def level3
raise "level3で発生したエラーです"
end
のようにしても、RuntimeErrorとしてraiseされます。つまり出力結果は同じ。
しかし、
def level3
raise Exception.new("level3で発生したエラーです")
end
としてしまうと、途中のlevel2,level1のrescueでは、例外をキャッチしてくれず、最初のbegin〜rescueのrescue => eで例外がキャッチされます。rescueだけだと、Exceptionはキャッチされないことに注意。
バックトレースの取りこぼしをなくすためにも、基本的にアプリケーションで使う例外は、RuntimeErrorもしくは、そのサブクラスにしましょう。rescueだけを書くというケースもあまりなさそうですけど。