Đây là pattern phổ biến trong Ruby: rescue một ngoại lệ và re-raise loại ngoại lệ khác. ActionView là ví dụ rõ ràng. Như tôi đã nói trong bài trước về TracePoint, ActionView nuốt bất kỳ ngoại lệ nào xảy ra trong template và re-raise chúng thành ActionView::TemplateError.
Đôi khi điều này không đủ. Bạn THỰC SỰ cần ngoại lệ gốc vì nó chứa dữ liệu giúp giải quyết vấn đề. May mắn thay, từ Ruby 2.1, bạn có thể dùng phương thức Exception#cause để làm chính điều đó.
Hãy xem nó hoạt động thực tế như thế nào. Ở đây, chúng ta raise NoMethodError, ngay lập tức nuốt nó và raise RuntimeError. Sau đó catch RuntimeError và dùng #cause để lấy NoMethodError gốc.
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 và custom attributes
Phương thức #cause thực sự trả về đối tượng ngoại lệ gốc. Nghĩa là bạn có thể truy cập bất kỳ metadata nào là một phần của ngoại lệ gốc. Bạn cũng có thể lấy backtrace gốc.
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
Đến vô cùng và hơn thế!
Mặc dù ví dụ trên chỉ sâu một cấp, nested exceptions trong Ruby có thể có bất kỳ số cấp nào. Tôi sẽ ngạc nhiên nếu bạn bao giờ cần đi sâu hơn ba bốn cấp.
...nhưng chỉ cho vui tôi thử làm nested exception 100 cấp. Đây là đoạn code ngớ ngẩn, và hy vọng bạn không bao giờ thấy nó trong 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() # ... In ra: # Level 100 # Level 99 # Level 98 # etc.