How do I add information to an exception message in Ruby?

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

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)


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...

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).