66

How do I add information to an exception message without changing its class in ruby?

The approach I'm currently using is

strings.each_with_index do |string, i|
  begin
    do_risky_operation(string)
  rescue
    raise $!.class, "Problem with string number #{i}: #{$!}"
  end
end

Ideally, I would also like to preserve the backtrace.

Is there a better way?

Sim
  • 13,147
  • 9
  • 66
  • 95
Andrew Grimm
  • 78,473
  • 57
  • 200
  • 338

10 Answers10

107

To reraise the exception and modify the message, while preserving the exception class and its backtrace, simply do:

strings.each_with_index do |string, i|
  begin
    do_risky_operation(string)
  rescue Exception => e
    raise $!, "Problem with string number #{i}: #{$!}", $!.backtrace
  end
end

Which will yield:

# RuntimeError: Problem with string number 0: Original error message here
#     backtrace...
Ryenski
  • 9,582
  • 3
  • 43
  • 47
  • 7
    On the line beginning with `raise`, is there a reason to use `$!` instead of `e`? They're the same object. – Jordan Running Jan 19 '15 at 06:34
  • 1
    @Jordan `e === $!` so yes you could use `e` instead of `$1`, assuming `e` is defined. The advantage of `$!` is that it is always available in an Exception block. Also of note is that `$@ === e.backtrace`. – Ryenski Mar 31 '15 at 17:45
  • 3
    You can also use `e2 = e.class.new "Foo: #{e}"` to create a new exception of the same type, and then `e2.set_backtrace(e.backtrace)` to take the backtrace from the original exception. – Matt Zukowski May 13 '15 at 18:30
  • wish I could give a +++ for this answer as I learned a lot from it (and the follow up comments) – Mitch VanDuyn Jan 15 '16 at 17:14
  • 8
    Note that rescuing `Exception` is probably not what you want https://stackoverflow.com/questions/10048173/why-is-it-bad-style-to-rescue-exception-e-in-ruby. – mlovic Jan 15 '18 at 11:53
  • 10
    Also note that `$!` is a _global variable_ pointing to the last exception raised in running Ruby program. It could be overwritten during the rescue block by another exception being raised in another thread. That is one reason to prefer using `e`. – mlovic Jan 15 '18 at 11:57
  • @mlovic In this case, where the Exception immediately gets re-raised, it _is_ probably what you want. – Qqwy Jun 20 '19 at 15:24
  • At some point, the code that prints the exception to STDERR for an exception that isn't caught now prints out the previous exception as well. All the methods on this page raise a new exception and thus the print out now has two stacks. I've yet to see a way to raise the same exception with an appended message. – pedz Apr 08 '20 at 17:49
19

It's not much better, but you can just reraise the exception with a new message:

raise $!, "Problem with string number #{i}: #{$!}"

You can also get a modified exception object yourself with the exception method:

new_exception = $!.exception "Problem with string number #{i}: #{$!}"
raise new_exception
Chuck
  • 234,037
  • 30
  • 302
  • 389
  • The first snippet is what I'm after. Looking at the documentation for `Kernel#raise`, it says that if you have more than one argument, the first item can be either an `Exception` class, or an object that returns an exception when `.exception` is called. I think my brain has just had an exception. – Andrew Grimm May 13 '10 at 01:00
  • but wouldn't this nix the original message? Wouldn't @Mark Rushakoff's approach be more conservative? – Dan Rosenstark May 13 '10 at 10:46
  • 2
    @yar: No, this doesn't nix the original message. That's the entire purpose of the `#{$!}` interpolation. It would nix the original message if you left that out, just like if you left out the call to `super` in Mark's method. I would frankly say my way is more conservative, since it's just using the language's intended exception re-raising mechanisms, whereas Mark's solution involves creating a whole module and redefining the `message` method just to get the same effect. – Chuck May 13 '10 at 17:13
  • Okay, thanks for the explanation, that's actually quite cool. I didn't realize that `raise` can take more than one param... I should've guessed that re-raising errors was already contemplated in Ruby, as it is a necessary thing. – Dan Rosenstark May 13 '10 at 17:51
  • 1
    Note that this will alter the backtrace, unless `$!.backtrace` is passed as the 3rd argument – Jason Denney Feb 27 '19 at 18:26
12

I realize I'm 6 years late to this party, but...I thought I understood Ruby error handling until this week and ran across this question. While the answers are useful, there is non-obvious (and undocumented) behavior that may be useful to future readers of this thread. All code was run under ruby v2.3.1.

@Andrew Grimm asks

How do I add information to an exception message without changing its class in ruby?

and then provides sample code:

raise $!.class, "Problem with string number #{i}: #{$!}"

I think it is critical to point out that this does NOT add information to the original error instance object, but instead raises a NEW error object with the same class.

@BoosterStage says

To reraise the exception and modify the message...

but again, the provided code

raise $!, "Problem with string number #{i}: #{$!}", $!.backtrace

will raise a new instance of whatever error class is referenced by $!, but it will not be the exact same instance as $!.

The difference between @Andrew Grimm's code and @BoosterStage's example is the fact that the first argument to #raise in the first case is a Class, whereas in the second case it is an instance of some (presumably) StandardError. The difference matters because the documentation for Kernel#raise says:

With a single String argument, raises a RuntimeError with the string as a message. Otherwise, the first parameter should be the name of an Exception class (or an object that returns an Exception object when sent an exception message).

If only one argument is given and it is an error object instance, that object will be raised IF that object's #exception method inherits or implements the default behavior defined in Exception#exception(string):

With no argument, or if the argument is the same as the receiver, return the receiver. Otherwise, create a new exception object of the same class as the receiver, but with a message equal to string.to_str.

As many would guess:

catch StandardError => e
  raise $!

raises the same error referenced by $!, the same as simply calling:

catch StandardError => e
  raise

but probably not for the reasons one might think. In this case, the call to raise is NOT just raising the object in $!...it raises the result of $!.exception(nil), which in this case happens to be $!.

To clarify this behavior, consider this toy code:

class TestError < StandardError
    def initialize(message=nil)
        puts 'initialize'
        super
    end
    def exception(message=nil)
        puts 'exception'
        return self if message.nil? || message == self
        super
    end
end

Running it (this is the same as @Andrew Grimm's sample which I quoted above):

2.3.1 :071 > begin ; raise TestError, 'message' ; rescue => e ; puts e ; end

results in:

initialize
message

So a TestError was initialized, rescued, and had its message printed. So far so good. A second test (analogous to @BoosterStage's sample quoted above):

2.3.1 :073 > begin ; raise TestError.new('foo'), 'bar' ; rescue => e ; puts e ; end

The somewhat surprising results:

initialize
exception
bar

So a TestError was initialized with 'foo', but then #raise has called #exception on the first argument (an instance of TestError) and passed in the message of 'bar' to create a second instance of TestError, which is what ultimately gets raised.

TIL.

Also, like @Sim, I am very concerned about preserving any original backtrace context, but instead of implementing a custom error handler like his raise_with_new_message, Ruby's Exception#cause has my back: whenever I want to catch an error, wrap it in a domain-specific error and then raise that error, I still have the original backtrace available via #cause on the domain-specific error being raised.

The point of all this is that--like @Andrew Grimm--I want to raise errors with more context; specifically, I want to only raise domain-specific errors from certain points in my app that can have many network-related failure modes. Then my error reporting can be made to handle the domain errors at the top level of my app and I have all the context I need for logging/reporting by calling #cause recursively until I get to the "root cause".

I use something like this:

class BaseDomainError < StandardError
  attr_reader :extra
  def initialize(message = nil, extra = nil)
    super(message)
    @extra = extra
  end
end
class ServerDomainError < BaseDomainError; end

Then if I am using something like Faraday to make calls to a remote REST service, I can wrap all possible errors into a domain-specific error and pass in extra info (which I believe is the original question of this thread):

class ServiceX
  def initialize(foo)
    @foo = foo
  end
  def get_data(args)
    begin
      # This method is not defined and calling it will raise an error
      make_network_call_to_service_x(args)
    rescue StandardError => e
      raise ServerDomainError.new('error calling service x', binding)
    end
  end
end

Yeah, that's right: I literally just realized I can set the extra info to the current binding to grab all local vars defined at the time the ServerDomainError is instantiated/raised. This test code:

begin
  ServiceX.new(:bar).get_data(a: 1, b: 2)
rescue
  puts $!.extra.receiver
  puts $!.extra.local_variables.join(', ')
  puts $!.extra.local_variable_get(:args)
  puts $!.extra.local_variable_get(:e)
  puts eval('self.instance_variables', $!.extra)
  puts eval('self.instance_variable_get(:@foo)', $!.extra)
end

will output:

#<ServiceX:0x00007f9b10c9ef48>
args, e
{:a=>1, :b=>2}
undefined method `make_network_call_to_service_x' for #<ServiceX:0x00007f9b10c9ef48 @foo=:bar>
@foo
bar

Now a Rails controller calling ServiceX doesn't particularly need to know that ServiceX is using Faraday (or gRPC, or anything else), it just makes the call and handles BaseDomainError. Again: for logging purposes, a single handler at the top level can recursively log all the #causes of any caught errors, and for any BaseDomainError instances in the error chain it can also log the extra values, potentially including the local variables pulled from the encapsulated binding(s).

I hope this tour has been as useful for others as it was for me. I learned a lot.

UPDATE: Skiptrace looks like it adds the bindings to Ruby errors.

Also, see this other post for info about how the implementation of Exception#exception will clone the object (copying instance variables).

mrtnrst
  • 41
  • 6
Lemon Cat
  • 1,002
  • 11
  • 14
  • At some point, the print out of an uncaught exception now prints out the previous exception as well. As you say, the various choices are creating and raising a new exception and thus, the output has two stacks. I've yet to find a way to raise the same exception with a modified message. – pedz Apr 08 '20 at 17:47
5

Here's another way:

class Exception
  def with_extra_message extra
    exception "#{message} - #{extra}"
  end
end

begin
  1/0
rescue => e
  raise e.with_extra_message "you fool"
end

# raises an exception "ZeroDivisionError: divided by 0 - you fool" with original backtrace

(revised to use the exception method internally, thanks @Chuck)

AlexChaffee
  • 8,092
  • 2
  • 49
  • 55
4

My approach would be to extend the rescued error with an anonymous module that extends the error's message method:

def make_extended_message(msg)
    Module.new do
      @@msg = msg
      def message
        super + @@msg
      end
    end
end

begin
  begin
      raise "this is a test"
  rescue
      raise($!.extend(make_extended_message(" that has been extended")))
  end
rescue
    puts $! # just says "this is a test"
    puts $!.message # says extended message
end

That way, you don't clobber any other information in the exception (i.e. its backtrace).

Mark Rushakoff
  • 249,864
  • 45
  • 407
  • 398
  • For those curious, if an exception occurs in `message`, you don't get a Stack Overflow, but if you were to do so in `Exception#initialize`, you do. – Andrew Grimm May 13 '10 at 03:02
  • 1
    And thanks to Ruby being dynamic yet strongly typed, it's easy to get an Exception in the `message` method. Just try to add a String and number together :) – Dan Rosenstark May 13 '10 at 10:48
  • 1
    @yar: Easily worked around by doing `super + String(@@msg)` or equivalent. – Mark Rushakoff May 13 '10 at 10:50
  • To be just a bit polemic, your first instinct was not to do that. And you probably wouldn't think of that in your unit tests either (the holy grail of dynamic langs). So someday it would blow up at runtime, and THEN you'd add that safeguard. – Dan Rosenstark May 13 '10 at 10:53
  • I'm not JUST complaining about dynamic langs: I'm also thinking about how to defensively program in them. – Dan Rosenstark May 13 '10 at 11:28
  • As another example of the hazards of dynamic-yet-strong typing, this will also fail if the exception did not already have a message, because NilClass doesn't define the `+` operator. It would probably be better in a lot of ways to use interpolation — `def message() "#{super} #{@@msg}" end`. – Chuck May 13 '10 at 17:25
  • Hmm, this is strange (and unfortunate). It seems that overridding message does not have any affect on the exception's other methods such as `to_s` and `inspect`. For example: `begin; 0/0; rescue; puts $!.extend(Module.new do; def message; super + ' With additional info!'; end; end).inspect; end # ` (I would have expected that `inspect` would have been defined in terms of message, such as `def inspect; "<#{self.class}: #{message}"; end`. Am I incorrect?) – Tyler Rick May 08 '15 at 20:38
  • Ah, I see. `message` is actually defined in terms of `to_s` (not the other way around): `return rb_funcall(exc, rb_intern("to_s"), 0, 0);` – Tyler Rick May 08 '15 at 21:10
2

I put my vote that Ryan Heneise's answer should be the accepted one.

This is a common problem in complex applications and preserving the original backtrace is often critical so much so that we have a utility method in our ErrorHandling helper module for this.

One of the problems we discovered was that sometimes trying to generate more meaningful messages when a system is in a messed up state would result in exceptions being generated inside the exception handler itself which led us to harden our utility function as follows:

def raise_with_new_message(*args)
  ex = args.first.kind_of?(Exception) ? args.shift : $!
  msg = begin
    sprintf args.shift, *args
  rescue Exception => e
    "internal error modifying exception message for #{ex}: #{e}"
  end
  raise ex, msg, ex.backtrace
end

When things go well

begin
  1/0
rescue => e
  raise_with_new_message "error dividing %d by %d: %s", 1, 0, e
end

you get a nicely modified message

ZeroDivisionError: error dividing 1 by 0: divided by 0
    from (irb):19:in `/'
    from (irb):19
    from /Users/sim/.rvm/rubies/ruby-2.0.0-p247/bin/irb:16:in `<main>'

When things go badly

begin
  1/0
rescue => e
  # Oops, not passing enough arguments here...
  raise_with_new_message "error dividing %d by %d: %s", e
end

you still don't lose track of the big picture

ZeroDivisionError: internal error modifying exception message for divided by 0: can't convert ZeroDivisionError into Integer
    from (irb):25:in `/'
    from (irb):25
    from /Users/sim/.rvm/rubies/ruby-2.0.0-p247/bin/irb:16:in `<main>'
Community
  • 1
  • 1
Sim
  • 13,147
  • 9
  • 66
  • 95
  • Why are you rescuing from the `Exception` class, rather than `StandardError`? – Andrew Grimm Sep 01 '13 at 01:46
  • 1
    @AndrewGrimm rescuing `StandardError` does not cover the kinds of edge cases that tend to occur with greater frequency when the proverbial hits the fan. Good examples are `LoadError` and `NotImplementedError`. We've also had cases of `#to_s` causing stack overflows as they smartly try to produce meaningful messages in complex data structures. You just don't know what will happen. – Sim Sep 01 '13 at 02:33
1

Most of these answers are incredibly convoluted. Maybe they were necessary in Ruby 1.8 or whatever, but in modern versions* this is totally straightforward and intuitive. Just rescue => e, append to e.message, and raise.

begin
  raise 'oops'
rescue => e
  e.message << 'y daisy'
  raise
end
Traceback (most recent call last):
        4: from /Users/david/.rvm/rubies/ruby-2.7.2/bin/irb:23:in `<main>'
        3: from /Users/david/.rvm/rubies/ruby-2.7.2/bin/irb:23:in `load'
        2: from /Users/david/.rvm/rubies/ruby-2.7.2/lib/ruby/gems/2.7.0/gems/irb-1.2.6/exe/irb:11:in `<top (required)>'
        1: from (irb):2
RuntimeError (oopsy daisy)

* I've only tested with 2.7.2 and 3.1.2, but I assume everything in between is covered, and probably some earlier versions of 2.x as well.

David Moles
  • 48,006
  • 27
  • 136
  • 235
  • What about adding message to `ZeroDivisionError` for example? – mechnicov Dec 02 '22 at 09:31
  • @mechnicov The above pattern works fine for me with ZDE in Ruby 3.1.2 for macOS -- just replace `raise 'oops'` with `puts 1 / 0`, and you get `(irb):46:in \`/': divided by 0ydaisy (ZeroDivisionError)` – David Moles Dec 04 '22 at 01:16
0

Here's what I ended up doing:

Exception.class_eval do
  def prepend_message(message)
    mod = Module.new do
      define_method :to_s do
        message + super()
      end
    end
    self.extend mod
  end

  def append_message(message)
    mod = Module.new do
      define_method :to_s do
        super() + message
      end
    end
    self.extend mod
  end
end

Examples:

strings = %w[a b c]
strings.each_with_index do |string, i|
  begin
    do_risky_operation(string)
  rescue
    raise $!.prepend_message "Problem with string number #{i}:"
  end
end
=> NoMethodError: Problem with string number 0:undefined method `do_risky_operation' for main:Object

and:

pry(main)> exception = 0/0 rescue $!
=> #<ZeroDivisionError: divided by 0>
pry(main)> exception = exception.append_message('. With additional info!')
=> #<ZeroDivisionError: divided by 0. With additional info!>
pry(main)> exception.message
=> "divided by 0. With additional info!"
pry(main)> exception.to_s
=> "divided by 0. With additional info!"
pry(main)> exception.inspect
=> "#<ZeroDivisionError: divided by 0. With additional info!>"

This is similar to Mark Rushakoff's answer but:

  1. Overrides to_s instead of message since by default message is defined as simply to_s (at least in Ruby 2.0 and 2.2 where I tested it)
  2. Calls extend for you instead of making the caller do that extra step.
  3. Uses define_method and a closure so that the local variable message can be referenced. When I tried using a class variable @@message, it warned, "warning: class variable access from toplevel" (See this question: "Since you're not creating a class with the class keyword, your class variable is being set on Object, not [your anonymous module]")

Features:

  • Easy to use
  • Reuses the same object (instead of creating a new instance of the class), so things like object identity, class, and backtrace are preserved
  • to_s, message, and inspect all respond appropriately
  • Can be used with an exception that is already stored in a variable; doesn't require you to re-raise anything (like the solution that involved passing the backtrace to raise: raise $!, …, $!.backtrace). This was important to me since the exception was passed in to my logging method, not something I had rescued myself.
Community
  • 1
  • 1
Tyler Rick
  • 9,191
  • 6
  • 60
  • 60
0

It's possible to use :cause key to prevent message duplication

The cause of the generated exception (accessible via Exception#cause) is automatically set to the "current" exception ($!), if any. An alternative value, either an Exception object or nil, can be specified via the :cause argument.

begin
  do_risky_operation
rescue => e
  raise e.class, "#{e.message} (some extra message)", e.backtrace, cause: nil
end
mechnicov
  • 12,025
  • 4
  • 33
  • 56
-1

Another approach would be to add context (extra information) about the exception as a hash instead of as a string.

Check out this pull request where I proposed adding a few new methods to make it really easy to add extra context information to exceptions, like this:

begin
  …
  User.find_each do |user|
    reraise_with_context(user: user) do
      send_reminder_email(user)
    end
  end
  …

rescue
  # $!.context[:user], etc. is available here
  report_error $!, $!.context
end

or even:

User.find_each.reraise_with_context do |user|
  send_reminder_email(user)
end

The nice thing about this approach is that it lets you add extra information in a very concise way. And it doesn't even require you to define new exception classes inside which to wrap the original exceptions.

As much as I like @Lemon Cat's answer for many reasons, and it's certainly appropriate for some cases, I feel like if what you are actually trying to do is attach additional information about the original exception, it seems preferable to just attach it directly to that exception it pertains to rather than inventing a new wrapper exception (and adding another layer of indirection).

Another example:

class ServiceX
  def get_data(args)
    reraise_with_context(StandardError, binding: binding, service: self.class, callee: __callee__) do
      # This method is not defined and calling it will raise an error
      make_network_call_to_service_x(args)
    end
  end
end

The downside of this approach is that you have to update your error handling to actually use the information that may be available in exception.context. But you would have to do that anyway in order to recursively call cause to get to the root excetion.

Tyler Rick
  • 9,191
  • 6
  • 60
  • 60