RAII in Ruby (Or, How to Manage Resources in Ruby) - ruby

I know it's by design that you can't control what happens when an object is destroyed. I am also aware of defining some class method as a finalizer.
However is the ruby idiom for C++'s RAII (Resources are initialized in constructor, closed in destructor)? How do people manage resources used inside objects even when errors or exceptions happen?
Using ensure works:
f = File.open("testfile")
begin
# .. process
rescue
# .. handle error
ensure
f.close unless f.nil?
end
but users of the class have to remember to do the whole begin-rescue-ensure chacha everytime the open method needs to be called.
So for example, I'll have the following class:
class SomeResource
def initialize(connection_string)
#resource_handle = ...some mojo here...
end
def do_something()
begin
#resource_handle.do_that()
...
rescue
...
ensure
end
def close
#resource_handle.close
end
end
The resource_handle won't be closed if the exception is cause by some other class and the script exits.
Or is the problem more of I'm still doing this too C++-like?

So that users don't "have to remember to do the whole begin-rescue-ensure chacha" combine rescue/ensure with yield.
class SomeResource
...
def SomeResource.use(*resource_args)
# create resource
resource = SomeResource.new(*resource_args) # pass args direct to constructor
# export it
yield resource
rescue
# known error processing
...
ensure
# close up when done even if unhandled exception thrown from block
resource.close
end
...
end
Client code can use it as follows:
SomeResource.use(connection_string) do | resource |
resource.do_something
... # whatever else
end
# after this point resource has been .close()d
In fact this is how File.open operates - making the first answer confusing at best (well it was to my work colleagues).
File.open("testfile") do |f|
# .. process - may include throwing exceptions
end
# f is guaranteed closed after this point even if exceptions are
# thrown during processing

How about yielding a resource to a block? Example:
File.open("testfile") do |f|
begin
# .. process
rescue
# .. handle error
end
end

Or is the problem more of I'm still doing this too C++-like?
Yes it is since in C++ resource deallocation happens implicitly for everything on the stack. Stack unwound = resource destroyed = destructors called and from there things can be released. Since Ruby has no destructors there is no "do that when everything else is done with" place since grabage collection can be delayed several cycles from where you are. You do have finalizers but they are called "in limbo" (not everything is available to them) and they get called on GC.
Therefore if you are holding a handle to some resource that better be released you need to release it explicitly. Indeed the correct idiom to handle this kind of situation is
def with_shmoo
handle = allocate_shmoo
yield(handle)
ensure
handle.close
end

See http://www.rubycentral.com/pickaxe/tut_exceptions.html
In Ruby, you would use an ensure statement:
f = File.open("testfile")
begin
# .. process
rescue
# .. handle error
ensure
f.close unless f.nil?
end
This will be familiar to users of Python, Java, or C# in that it works like try / catch / finally.

Related

How to prevent problems with `return` from block when using Ruby `yield`

As every Ruby programmer eventually discovers, calling blocks or procs that contain return statements can be dangerous as this might exit your current context:
def some_method(&_block)
puts 1
yield
# The following line will never be executed in this example
# as the yield is actually a `yield-and-return`.
puts 3
end
def test
some_method do
puts 2
return
end
end
test
# This prints "1\n2\n" instead of "1\n2\n3\n"
In cases you want to be absolutely sure some of your code runs after you called a block or proc, you can use a begin ... ensure construct. But since ensure is also called if there is an exception during yield, it requires a little more work.
I've created a tiny module that deals with this problem in two different ways:
Using safe_yield, it is detected whether the yielded block or proc actually returns using the return keyword. If so, it raises an exception.
unknown_block = proc do
return
end
ReturnSafeYield.safe_yield(unknown_block)
# => Raises a UnexpectedReturnException exception
Using call_then_yield, you can call a block and then make sure that a second block is executed, even if the first block contains a return statement.
unknown_block = proc do
return
end
ReturnSafeYield.call_then_yield(unknown_block) do
# => This line is called even though the above block contains a `return`.
end
I'm considering to create a quick Gem out of this, or is there any built-in solution to prevent quick return from the nested block which I missed?
There is a built-in solution to detect whether a block contains a return statement.
You can use RubyVM::InstructionSequence.disasm to disassemble the block passed in by the user, then search it for throw 1, which represents a return statement.
Here's a sample implementation:
def safe_yield(&block)
if RubyVM::InstructionSequence.disasm(block) =~ /^\d+ throw +1$/
raise LocalJumpError
end
block.call
end
Here's how you might incorporate it into your library:
def library_method(&block)
safe_yield(&block)
puts "library_method succeeded"
rescue LocalJumpError
puts "library_method encountered illegal return but resumed execution"
end
And here's the user experience for a well-behaved and a misbehaving user:
def nice_user_method
library_method { 1 + 1 }
end
nice_user_method
# library_method succeeded
def naughty_user_method
library_method { return false if rand > 0.5 }
end
naughty_user_method
# library_method encountered illegal return but resumed execution
Commentary:
Using raise LocalJumpError/rescue LocalJumpError gets around the issues you encountered when using a blanket ensure.
I chose LocalJumpError because it seems relevant, and because (I think!) there is no possible Ruby code that would result in LocalJumpError being raised "naturally" in this context. If that turns out to be false, you can easily substitute your own new exception class.

Decorating an object's interface with additional behavior in Ruby?

I have a Ruby object that dispatches events to a third-party service:
class Dispatcher
def track_event(e)
ThirdPartyService.track e.id, e.name, my_api_key
end
end
The use of ThirdPartyService may raise errors (e.g. if no network connection is available). It's usually appropriate for consumers of Dispatcher to decide how to deal with these, rather than Dispatcher itself.
How could we decorate the use of Dispatcher in such a way that objects appear to be using a Dispatcher, but all exceptions are caught and logged instead? That is, I want to be able to write:
obj.track_event(...)
but have exceptions be caught.
Thoughtbot has a great blog post on different ways to implement decorators in Ruby. This one ("Module + Extend + Super decorator") is especially succinct:
module ErrorLoggingMixin
def track_event(event)
super
rescue ex
Logger.warn(ex)
end
end
obj = Dispatcher.new # initialize a Dispatcher as usual
obj.extend(ErrorLoggingMixin) # extend it with the new behavior
obj.track_event(some_event) # call its methods as usual
The blog post lists these pros and cons:
The benefits of this implementation are:
it delegates through all decorators
it has all of the original interface because it is the original object
The drawbacks of this implementation are:
can not use the same decorator more than once on the same object
*difficult to tell which decorator added the functionality
I recommend reading the rest of the post to see the other implementations and make the choice that best fits your needs.
The closest idea I've got is to surround the usage of track_event in a method which provides a block that catches exceptions, like this:
module DispatcherHelper
def self.dispatch(&block)
dispatcher = Dispatcher.new
begin
yield dispatcher
rescue NetworkError
# ...
rescue ThirdPartyError
# ...
end
end
end
so that we can then do:
DispatcherHelper.dispatch { |d| d.track_event(...) }
The other alternative I can see is to mimic the signature of track_event, so that you get:
module DispatcherHelper
def self.track_event(e)
begin
Dispatcher.new.track_event(e)
rescue NetworkError
# ...
rescue ThirdPartyError
# ...
end
end
end
but I'm less fond of that since it ties the signatures together.
Use a Bang Method
There's more than one way to do this. One way to accomplish your goal would be a small refactoring to redefine the exception-raising method as a "cautionary" method, make it private, and use the "safer" bang-free method in the public interface. For example:
class Dispatcher
def track_event(e)
result = track_event! e rescue nil
result ? result : 'Exception handled here.'
end
private
def track_event! e
ThirdPartyService.track e.id, e.name, my_api_key
end
end
Using the refactored code would yield the following when an exception is raised by ThirdPartyService:
Dispatcher.new.track_event 1
#=> "Exception handled here."
There are certainly other ways to address this type of problem. A lot depends on what your code is trying to express. As a result, your mileage may vary.

Rspec Ruby Mocking

I would like to achieve 100% coverage on a module. My problem is that there is a variable (called data) within a method which I am trying to inject data in to test my exception handling. Can this be done with mocking? If not how can i fully test my exception handling?
module CSV
module Extractor
class ConversionError < RuntimeError; end
class MalformedCSVError < RuntimeError; end
class GenericParseError < RuntimeError; end
class DemoModeError < RuntimeError; end
def self.open(path)
data = `.\\csv2text.exe #{path} -f xml --xml_output_styles 2>&1`
case data
when /Error: Wrong input filename or path:/
raise MalformedCSVError, "the CSV path with filename '#{path}' is malformed"
when /Error: A valid password is required to open/
raise ConversionError, "Wrong password: '#{path}'"
when /CSVTron CSV2Text: This page is skipped when running in the demo mode./
raise DemoModeError, "CSV2TEXT.exe in demo mode"
when /Error:/
raise GenericParseError, "Generic Error Catch while reading input file"
else
begin
csvObj = CSV::Extractor::Document.new(data)
rescue
csvObj = nil
end
return csvObj
end
end
end
end
Let me know what you think! Thanks
===================== EDIT ========================
I have modified my methods to the design pattern you suggested. This method-"open(path)" is responsible for trapping and raising errors, get_data(path) just returns data, That's it! But unfortunately in the rspec I am getting "exception was expected to be raise but nothing was raised." I thought maybe we have to call the open method from your stub too?
This is what I tried doing but still no error was raised..
it 'should catch wrong path mode' do
obj = double(CSV::Extractor)
obj.stub!(:get_data).and_return("Error: Wrong input filename or path:")
obj.stub!(:open)
expect {obj.open("some fake path")}.to raise_error CSV::Extractor::MalformedCSVError
end
Extract the code that returns the data to a separate method. Then when you test open you can stub out that method to return various strings that will exercise the different branches of the case statement. Roughly like this for the setup:
def self.get_data(path)
`.\\csv2text.exe #{path} -f xml --xml_output_styles 2>&1`
end
def self.open(path)
data = get_data(path)
...
And I assume you know how to stub methods in rspec, but the general idea is like this:
foo = ...
foo.stub(:get_data).and_return("Error: Wrong input filename or path:")
expect { foo.get_data() }.to raise_error MalformedCSVError
Also see the Rspec documentation on testing for exceptions.
Problem with testing your module lies in the way you have designed your code. Think about splitting extractor into two classes (or modules, it's matter of taste -- I'd go with classes as they are a bit easier to test), of which one would read data from external system call, and second would expect this data to be passed as an argument.
This way you can easily mock what you currently have in data variable, as this would be simply passed as an argument (no need to think about implementation details here!).
For easier usage you can later provide some wrapper call, that would create both objects and pass one as argument to another. Please note, that this behavior can also be easily tested.

Ruby: Destructors?

I need to occasionaly create images with rmagick in a cache dir.
To then get rid of them fast, without loosing them for the view, I want to delete the image-files while my Ruby Instance of the Image-Class get's destructed or enters the Garbage Collection.
What ClassMethod must I overwrite to feed the destructor with code?
#edgerunner's solution almost worked. Basically, you cannot create a closure in place of the define_finalizer call since that captures the binding of the current self. In Ruby 1.8, it seems that you cannot use any proc object converted (using to_proc) from a method that is bound to self either. To make it work, you need a proc object that doesn't capture the object you are defining the finalizer for.
class A
FINALIZER = lambda { |object_id| p "finalizing %d" % object_id }
def initialize
ObjectSpace.define_finalizer(self, self.class.method(:finalize)) # Works in both 1.9.3 and 1.8
#ObjectSpace.define_finalizer(self, FINALIZER) # Works in both
#ObjectSpace.define_finalizer(self, method(:finalize)) # Works in 1.9.3
end
def self.finalize(object_id)
p "finalizing %d" % object_id
end
def finalize(object_id)
p "finalizing %d" % object_id
end
end
a = A.new
a = nil
GC.start
You can use ObjectSpace.define_finalizer when you create the image file, and it will get invoked when the garbage man comes to collect. Just be careful not to reference the object itself in your proc, otherwise it won't be collected by the garbage man. (Won't pick up something that's alive and kicking)
class MyObject
def generate_image
image = ImageMagick.do_some_magick
ObjectSpace.define_finalizer(self, proc { image.self_destruct! })
end
end
GC quirks are nice to read about, but why not properly deallocate resources according to already existing language syntax?
Let me clarify that.
class ImageDoer
def do_thing(&block)
image= ImageMagick.open_the_image # creates resource
begin
yield image # yield execution to block
rescue
# handle exception
ensure
image.destruct_sequence # definitely deallocates resource
end
end
end
doer= ImageDoer.new
doer.do_thing do |image|
do_stuff_with_image # destruct sequence called if this throws
end # destruct_sequence called if execution reaches this point
Image is destroyed after the block finishes executing. Just start a block, do all the image processing inside, then let the image destroy itself. This is analogous to the following C++ example:
struct Image
{
Image(){ /* open the image */ }
void do_thing(){ /* do stuff with image */ }
~Image(){ /* destruct sequence */ }
};
int main()
{
Image img;
img.do_thing(); // if do_thing throws, img goes out of scope and ~Image() is called
} // special function ~Image() called automatically here
Ruby has ObjectSpace.define_finalizer to set finalizers on objects, but its use isn't exactly encouraged and it's rather limited (e.g. the finalizer can't refer to the object it is set for or else the finalizer will render the object ineligible for garbage collection).
There's really no such thing as a destructor in Ruby.
What you could do is simply clear out any files that are no longer open, or use the TempFile class which does this for you.
Update:
I previously claimed that PHP, Perl and Python do not have destructors, but this does appear to be false as igorw points out. I have not seen them used very often, though. A properly constructed destructor is essential in any allocation-based language, but in a garbage collected one it ends up being optional.
There is very simple solution for your problem. Ruby design encourage you to do all actions in definite and clear way. No need for magic actions in constructor/destructor. Yes, constructors are required as a convenient way to assign initial state of object but not for "magic" actions. Let me illustrate this approach on possible solution.
Goal, to keep image objects available but clean cache files of images.
# you are welcome to keep an in memory copy of the image
# GC will take care of it.
class MyImage
RawPNG data
end
# this is a worker that does operations on the file in cache directory.
# It knows presizely when the file can be removed (generate_image_final)
# no need to wait for destructor ;)
class MyImageGenerator
MyImage #img
def generate_image_step1
#image_file = ImageLib.create_file
end
def generate_image_step2
ImageLib.draw #image_file
end
def generate_image_final
#img=ImageLib.load_image #image_file
delete_that_file #image_file
end
def getImage
# optional check image was generated
return #img
end
end

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

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?
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...
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
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).
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)
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).
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>'
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:
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)
Calls extend for you instead of making the caller do that extra step.
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.
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.
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.
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

Resources