Feature #19197
openAdd Exception#root_cause
Description
Description¶
I would like to add a #root_cause
method to Exception
.
It returns the last exception in linked-list of causality chain, that is, the original exception (whose own cause
is nil
).
Example¶
e = begin
raise 'A' # This is the root cause
rescue => a
begin
raise 'B'
rescue => B
begin
raise 'C' # This is the outermost cause assigned to `e`
rescue => c
c
end
end
end
# Here's what the structure looks like:
# C -> B -> A -> nil
p(e) # => #<RuntimeError: C>
p(e.cause) # => #<RuntimeError: B>
p(e.cause.cause) # => #<RuntimeError: A>
p(e.cause.cause.cause) # => nil
# Here's the proposed API, showing that A is the root cause of e
p(e.root_cause) # => #<RuntimeError: A>
# And that the root_cause has no further cause
p(e.root_cause.cause) # => nil
Motivation¶
There are some kinds of exceptions that can occur all over the place (and might be wrapped by arbitrarily many middlemen), but are attributable to a singular global cause. For example, a database outage could raise exceptions in almost every line of business logic of an app that uses ActiveRecord models.
Fundamentally, you wouldn't want an error report for every one of these lines. You'd want to look at the root cause, and bucket all SQL-connection issues into a single report, regardless of where they surface.
Implementation¶
Draft PR: https://github.com/ruby/ruby/pull/6913
Updated by AMomchilov (Alexander Momchilov) almost 2 years ago
- Description updated (diff)
Updated by rubyFeedback (robert heiler) over 1 year ago
I do not have any particular opinion on the issue as such,
but on the issue of object.cause.cause.cause
(object.method1.method1.method1). From an API point of view
I think this is normally not "the ruby way" when it leads
to repetition. Usually ruby favours being expressive in
what one does, e. g. collection.take(10) or .first(20) or
.last(30).
you wouldn't want an error report for every one of these
lines.
If I understand it correctly you prefer more control over
the error report? If so then I think that makes sense; mame
improved on the error feedback ruby gives, if I recall
correctly. I now get a lot more information about where an
error happens, including a follow-up trace. I don't remember
this in the ruby 1.8.x era, for instance.
Updated by Eregon (Benoit Daloze) over 1 year ago
I think this makes sense and it's pretty trivial.
I think you need to add this to a dev meeting ticket so it will be decided whether it's accepted or not.
Updated by AMomchilov (Alexander Momchilov) over 1 year ago
- Description updated (diff)
Updated by AMomchilov (Alexander Momchilov) over 1 year ago
Hey Robert, thanks for taking the time to write.
rubyFeedback (robert heiler) wrote in #note-2:
but on the issue of object.cause.cause.cause
(object.method1.method1.method1). From an API point of view
I think this is normally not "the ruby way" when it leads
to repetition.
Ah, I wasn't suggesting to use this style of code, it was just a very simple/concise demonstration of the structure of the sample exception I made. I've updated my post to clarify that.
rubyFeedback (robert heiler) wrote in #note-2:
If I understand it correctly you prefer more control over
the error report?
Correct!
rubyFeedback (robert heiler) wrote in #note-2:
If so then I think that makes sense; mame
improved on the error feedback ruby gives, if I recall
correctly. I now get a lot more information about where an
error happens, including a follow-up trace. I don't remember
this in the ruby 1.8.x era, for instance.
Sorry, I don't understand what you're trying to say here
Updated by AMomchilov (Alexander Momchilov) over 1 year ago
Eregon (Benoit Daloze) wrote in #note-3:
I think this makes sense and it's pretty trivial.
I think you need to add this to a dev meeting ticket so it will be decided whether it's accepted or not.
Hey Benoit!
What's a "dev meeting ticket"? Done!
Also, what do you think of some alternative spellings, like having a #causes: Array[Exception]
, on which you could just call #last
?
E.g.
p e.causes.last # The root cause
Alternatively, you could do this song-dance:
p Enumerator.produce(e) { |e| e.cause or raise StopIteration }.to_a.last
Interestingly, there's Enumerable#first
, but not Enumerable#last
, so you have to go through #to_a
(since there is a Array#last
). Perhaps that should be its own pitch 😅
Updated by Eregon (Benoit Daloze) over 1 year ago
I think more complex cases like causes
should be done manually or with a helper method, like you showed or with some loop or something.
OTOH I think root_cause
is convenient and useful for a variety of cases, so it makes more sense to be in core.
Updated by mame (Yusuke Endoh) over 1 year ago
Discussed at the dev meeting.
Please elaborate the use case a bit more. The proposed method is easy to implement in Ruby. To introduce it as a builtin feature, you need to prove that it is frequently needed in a variety of applications and libraries.
You'd want to look at the root cause, and bucket all SQL-connection issues into a single report
Do you have in mind an application monitor service like DataDog or Sentry? If so, what you need is not a Ruby method, but a request to ask the service.
Updated by AMomchilov (Alexander Momchilov) over 1 year ago
mame (Yusuke Endoh) wrote in #note-8:
Discussed at the dev meeting.
Please elaborate the use case a bit more.
I was experimenting around in an IRB session with some code that raised an exception wrapped by another exception. Actually getting to the root cause is pretty cumbersome. In a REPL environment, the best you can do is Enumerator.produce(e) { |e| e.cause or raise StopIteration }.to_a.last
, which is still a handfull.
Do you have in mind an application monitor service like DataDog or Sentry? If so, what you need is not a Ruby method, but a request to ask the service.
In our case it was Bugsnag. They do already have an API to let you decide how to group things by any arbitrary value. We wanted to group some things by root causes, and to do that we needed to iterate these error linked lists ourselves to get those ourselves.
The proposed method is easy to implement in Ruby.
Yep, and I did I write the Ruby code to get the root cause, and that's fine, but it just felt like a pretty basic (albeit niche) thing that should be built in.