Pattern ที่พบบ่อยใน Ruby คือ rescue exception แล้ว re-raise exception อีกชนิดหนึ่ง ActionView ตัวอย่างชัดเจน: โพสต์ก่อนเกี่ยว TracePoint กล่าวว่า ActionView นำ exceptions ใน templates ไป re-raise เป็น ActionView::TemplateError
บางครั้งไม่พอ คุณต้องการ original exception จริงๆ เพราะมี data ช่วยแก้ปัญหา โชคดีที่ Ruby 2.1+ มี Exception#cause ให้ใช้
มาดูการทำงานจริง: raise NoMethodError แล้วกลืนและ raise RuntimeError จับ RuntimeError ใช้ #cause ดึง NoMethodError ต้นทาง:
def fail_and_reraise
raise NoMethodError
rescue
raise RuntimeError
end
begin
fail_and_reraise
rescue => e
puts "#{ e } caused by #{ e.cause }"
end
Nested backtraces และ custom attributes
#cause คืน original exception object จริงๆ หมายความว่าเข้าถึง metadata ทั้งหมดของ original exception ได้ รวมถึง backtrace ต้นทาง
class EatingError < StandardError
attr_reader :food
def initialize(food)
@food = food
end
end
def fail_and_reraise
raise EatingError.new("soup")
rescue
raise RuntimeError
end
begin
fail_and_reraise
rescue => e
puts "#{ e } caused by #{ e.cause } while eating #{ e.cause.food }"
puts e.cause.backtrace.first
end
To infinity and beyond!
ตัวอย่างข้างต้นลึกแค่หนึ่งระดับ แต่ nested exceptions ใน Ruby ลึกได้ไม่จำกัด ผมคิดว่าน่าแปลกใจถ้าคุณต้องลึกเกิน 3-4 ระดับ
...แต่เพื่อสนุก ลองทำ 100-level deep nested exception โค้ดขี้เล่นนี้ หวังว่าไม่เจอใน production
def recursively_raise(c=0)
raise "Level #{ c }"
rescue => e
if c < 100
recursively_raise(c + 1)
else
recursively_print(e)
end
end
def recursively_print(e)
if e
puts e
recursively_print(e.cause)
end
end
recursively_raise()
# ... Prints the following:
# Level 100
# Level 99
# Level 98
# etc.