views:

42

answers:

2

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

Is there a better way?

+2  A: 

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
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
@Chuck, but wouldn't this nix the original message? Wouldn't @Mark Rushakoff's approach be more conservative?
Yar
@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
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.
Yar
+2  A: 

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
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
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 :)
Yar
@yar: Easily worked around by doing `super + String(@@msg)` or equivalent.
Mark Rushakoff
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.
Yar
I'm not JUST complaining about dynamic langs: I'm also thinking about how to defensively program in them.
Yar
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