Project

General

Profile

Actions

Feature #19197

open

Add Exception#root_cause

Added by AMomchilov (Alexander Momchilov) over 1 year ago. Updated 10 months ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:111264]

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

Actions #1

Updated by AMomchilov (Alexander Momchilov) over 1 year ago

  • Description updated (diff)

Updated by rubyFeedback (robert heiler) about 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) about 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.

Actions #4

Updated by AMomchilov (Alexander Momchilov) about 1 year ago

  • Description updated (diff)

Updated by AMomchilov (Alexander Momchilov) about 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) about 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) about 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) about 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) 10 months 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.

Actions

Also available in: Atom PDF

Like4
Like0Like0Like0Like0Like0Like0Like1Like0Like0