How can I refactor to get rid of an instance variable? - ruby

I have tons of methods like this:
def lab1
setup
business_processing
rescue Exception => e
handle_error('custom error message for lab1', e)
end
def lab2
setup
business_processing
rescue Exception => e
handle_error('custom errror message for lab2', e)
end
I refactored the code above into something like this:
def lab1
with_setup_and_error_handling do
#error_message = 'error in lab1'
business_processing
end
end
def with_setup_and_error_handling(&block)
setup
block.call
rescue Exception => e
handle_error(#error_message, e)
end
The new code works but it causes error_message to be an instance variable. Is there a better way to refactor it?
When someone looks at the refactored code they see the instance variable, but it is not clear where it is being used so I am not happy with it.

Put the error message to be a argument of de setup method:
def lab1
with_setup_and_error_handling('error in lab1') do
business_processing
end
end
def with_setup_and_error_handling(error_message, &block)
setup
block.call
rescue Exception => e
handle_error(error_message, e)
end

Why not just passing the error message as an argument to your with_setup_and_error_handling? Seems like an obvious solution to me.

This is an XY Problem*.
The fact that you think you need to "get rid of" an instance variable is a smell that indicates a poor design. In this case, the poor design is state leak caused by improper use/understanding of scope (instance variables). "It doesn't work otherwise" is not sufficient reason to make something an instance variable.
* http://www.perlmonks.org/index.pl?node_id=542341

If all you are changing is the error message per process then why not refactor the code the following way:
def lab(version)
begin
setup
business_processing
rescue Exception => e
handle_error("error in lab#{version}", e)
end
end
and if necessary to keep the individual labN methods do the following:
def lab1
self.lab 1
end
def lab2
self.lab 2
end

Related

Rescue from an error that may not be defined

My Rails website (this problem is purely Ruby based though) uses the AWS-SES (Action mailer using AWS) gem in test/development environment, and I am catching possible errors from email deliveries like this
def try_delivering_email(options = {})
begin
yield
return false
rescue EOFError,
...
AWS::SES::ResponseError,
... => e
log_exception(e, options)
return e
end
end
Now the problem is that this gem is only defined for specific environments, in other words AWS does not exist in development, and the error checking code will therefore throw an error (haha) for undefined constant.
I have tried substuting that line for (AWS::SES::ResponseError if defined?(AWS) but then the next error I get is
class or module required for rescue clause
How can I get around this in the nicest way possible ?
The exception list of a rescue-clause doesn't have to be a literal/static list:
excs = [EOFError]
defined?(AWS) && excs << AWS::SES::Response
# ...
rescue *excs => e
The splat operator * is used here to convert an array into a list.
You can't include a conditional in a rescue clause, but you can blind rescue and then get picky about how to deal with it using conventional Ruby code:
rescue EOFError => e
log_exception(e)
e
rescue => e
if (defined?(AWS) and e.is_a?(AWS::SES::Response))
# ...
else
raise e
end
end
It's not the nicest way, but it does the job. You could always encapsulate a lot of that into some module that tests more neatly:
def loggable_exception?(e)
case (e)
when EOFError, AnotherError, EtcError
true
else
if (defined?(AWS) and e.is_a?(AWS::SES::Response))
true
else
false
end
end
end
Then you can do this as that method name should be self-explanatory:
rescue => e
if (loggable_exception?(e))
log_exception(e)
e
else
raise e
end
end
You could make this a little neater if log_exception returned the exception it was given. Don't forget Ruby is "return by default" and it doesn't need to be explicit unless you're doing it early.

Reraise (same exception) after catching an exception in Ruby

I am trying to improve my Ruby skills by catching exceptions. I want to know if it is common to reraise the same kind of exception when you have several method calls. So, would the following code make sense? Is it ok to reraise the same kind of exception, or should I not catch it on the process method?
class Logo
def process
begin
#processed_logo = LogoProcessor::create_image(self.src)
rescue CustomException
raise CustomException
end
end
end
module LogoProcessor
def self.create_image
raise CustomException if some_condition
end
end
Sometimes we just want to know an error happened, without having to actually handle the error.
It is often the case that the one responsible for handling errors is user of the object: the caller. What if we are interested in the error, but don't want to assume that responsibility? We rescue the error, do whatever we need to do and then propagate the signal up the stack as if nothing had happened.
For example, what if we wanted to log the error message and then let the caller deal with it?
begin
this_will_fail!
rescue Failure => error
log.error error.message
raise
end
Calling raise without any arguments will raise the last error. In our case, we are re-raising error.
In the example you presented in your question, re-raising the error is simply not necessary. You could simply let it propagate up the stack naturally. The only difference in your example is you're creating a new error object and raising it instead of re-raising the last one.
This will raise the same type of error as the original, but you can customize the message.
rescue StandardError => e
raise e.class, "Message: #{e.message}"
I had the same question as in the comment thread here, i.e. What if the line before (re)raise fails?
My understanding was limited by the missing knowledge that the global variable of $! is "kinda garbage collected" // "scoped to its functional context", which the below example demonstrates:
def func
begin
raise StandardError, 'func!'
rescue StandardError => err
puts "$! = #{$!.inspect}"
end
end
begin
raise StandardError, 'oh no!'
rescue StandardError => err
func
puts "$! = #{$!.inspect}"
raise
end
The output of the above is:
$! = #<StandardError: func!>
$! = #<StandardError: oh no!>
StandardError: oh no!
from (pry):47:in `__pry__'
This behavior is different than how Python's (re)raise works.
The documentation for Exception states:
When an exception has been raised but not yet handled (in rescue,
ensure, at_exit and END blocks), two global variables are set:
$! contains the current exception.
$# contains its backtrace.
So these variables aren't true global variables, they are only defined inside the block that's handling the error.
begin
raise
rescue
p $! # StandardError
end
p $! # nil
A slightly better way to do the same thing as FreePender is to use the exception method from the Exception class, which is the ancestor class to any error classes, like StandardError, so that the method is available to any error classes.
Here the method's documentation that you can find on ApiDock:
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.
Now let's see how it works:
begin
this_will_fail!
rescue Failure => error
raise error.exception("Message: #{error.message}")
end
Adding to above answers here:
In some applications you may need to log the error twice.
For example exception need to be notified to monitoring tools like
Nagios/Newrelic/Cloudwatch.
At the same time you may have your own kibana backed summary logging
tool, internally for your reference.
In those cases you might want to log & handle the errors multiple times.
Example:
begin
begin
nil.to_sym
rescue => e
puts "inner block error message: #{e.message}"
puts "inner block backtrace: #{e.backtrace.join("\n")}"
raise e
end
rescue => e
puts "outer block error message: #{e.message}"
puts "outer block backtrace: #{e.backtrace.join("\n")}"
end
I am using puts here, for your the ease of verifying this code in
rails console, in actual production you may need to use rails logger

How to catch all exceptions as soon as they occur by just one rescue statement

I know how to catch the exceptions but what we do is to put "rescue" after a suspicious section of a code. what if you had a lot functions sending a query to mysql through mysql2 gem and you want to catch their exceptions. One thing you can do is to put a "rescue" statement in each of them. but i want to do that just by one rescue statement. So I put a "rescue" in end of code and put all of code in a "begin" and "end" but it didn't work.
Here is my code and as you see there is a problem in mysql query and just because of "rescue" being end of file, it doesn't catch the exception but when I put it after that query it works.
require 'mysql2'
require 'colored'
begin
def log(string)
p "["+string.cyan+"]"
end
def err
p "["+"FAIL".red+"]"
end
def done
p "["+"DONE".red+"]"
end
class SqlClient
def initialize()
log "SqlClient/initialize"
puts "Host: \n"
#host = gets.strip
puts "User: \n"
#user = gets.strip
puts "Pass: \n"
#pass = gets.strip
#client = Mysql2::Client.new(host: #host , username: #user , password: #pass)
end
def list_databases()
puts "We are listing your databases(not just projects) \n \n \n "
#client.query("ELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA").each do |row|
p row["SCHEMA_NAME"]
end
puts "\n \n \n"
end
end
rescue Mysql2::Error
err
abort
end
`query': You have an error in your SQL syntax; check the manual that corresponds to your
MySQL server version for the right syntax to use near 'ELECT SCHEMA_NAME FROM
INFORMATION_SCHEMA.SCHEMATA' at line 1 (Mysql2::Error)
I'm not looking for something like:
begin
# my code
rescue # this line is right after the code which is going to have problem and we catch it.
end
I'm looking for something like this:
begin
# first method
# second method
# thrid method
# rest of code and etc ...
# now this is end of file:
rescue
end
but as you saw in my code, it didn't work.
UPDATE: I found a similar question here and it seems there will be no answer :| maybe this is a sort of ruby Weakness.
if you want to see ANY error just use e for example
begin
# your call to a method of Mysql2 gem. for example:
client = Mysql2::Client.new(:host => "localhost", :username => "root", etc...)
rescue => e
puts e.message
puts e.backtrace.inspect
end
In order to catch every exception you'd need to wrap each method call with a begin rescue end. When an exception is raised it bails out of the execution so it wouldn't hit the next methods.
To catch all errors I guess I'd do something like this. Keep in mind, this is ugly and I'd recommend for you NOT to do this, but... if you want to try, maybe try something like this:
all_errors = []
# first method you call
begin
# some error might happen here
first_response = Mysql2::Client.new(:host => "localhost", :username => "root", etc...)
rescue => e
all_errors << e
end
# second method you call
begin
# some error might happen here
second_response = Mysql2::Client.new(:host => "localhost", :username => "root", etc...)
rescue => e
all_errors << e
end
puts all_errors.inspect
After a quick search I've found: (http://coderrr.wordpress.com/2008/11/07/the-simple-way-to-print-exceptions-in-ruby/)
# catch all exceptions (anything that derives from Exception)
begin
...
rescue Exception
puts $!, $#
end
You could use an at_exit handler, which has access to the last exception in $!
like
at_exit {
puts "the exception that killed us is", $!
}
If you want to catch Exceptions "as soon as they occur" (not after they're caught) you could use ruby's "debug mode" (which outputs messages when they occur to the console) or ruby-debug see Is there any way to start the Ruby debugger on exception?
Just wrap all your code in:
begin
#yourcode
#as much as you want
rescue
end
Nobody seemed to notice it but using rescue without a class will catch all StandardError and there are so much more.
If you want to catch ALL exceptions you need to do
begin
# your code where you call SqlClient.new etc
rescue Exception => e
puts "error raised"
puts [e, e.backtrace].flatten.join("\n")
end
List of all error classes:
Exception
NoMemoryError
ScriptError
LoadError
NotImplementedError
SyntaxError
SignalException
Interrupt
StandardError
ArgumentError
IOError
EOFError
IndexError
LocalJumpError
NameError
NoMethodError
RangeError
FloatDomainError
RegexpError
RuntimeError
SecurityError
SystemCallError
SystemStackError
ThreadError
TypeError
ZeroDivisionError
SystemExit
fatal
have you tried adding an at_exit method in your class? This would allow you to do something when ruby is exiting. like in this article.
Ruby at_exit
or
From Ruby 2.0 API Docs
However beware of cleverly rescuing from an exception!
You'll start pulling your hair out down the road (or another dev will) when you try to figure out why your code isn't failing when it should be failing. I prefer to fail massively with a bright shiny sign saying the code failed here! hehe.
Good luck!

How to rescue all exceptions under a certain namespace?

Is there a way to rescue all exceptions under a certain namespace?
For example, I want to rescue all of the Errno::* exceptions (Errno::ECONNRESET, Errno::ETIMEDOUT). I can go ahead and list them all out on my exception line, but I was wondering if I can do something like.
begin
# my code
rescue Errno
# handle exception
end
The above idea doesn't seem to work, thus is there something similar that can work?
All the Errno exceptions subclass SystemCallError:
Module Errno is created dynamically to map these operating system errors to Ruby classes, with each error number generating its own subclass of SystemCallError. As the subclass is created in module Errno, its name will start Errno::.
So you could trap SystemCallError and then do a simple name check:
rescue SystemCallError => e
raise e if(e.class.name.start_with?('Errno::'))
# do your thing...
end
Here is another interesting alternative. Can be adapted to what you want.
Pasting most interesting part:
def match_message(regexp)
lambda{ |error| regexp === error.message }
end
begin
raise StandardError, "Error message about a socket."
rescue match_message(/socket/) => error
puts "Error #{error} matches /socket/; ignored."
end
See the original site for ruby 1.8.7 solution.
It turns out lambda not accepted my more recent ruby versions. It seems the option is to use what worked in 1.8.7 but that's IM slower (to create a new class in all comparisons. So I don't recommend using it and have not even tried it:
def exceptions_matching(&block)
Class.new do
def self.===(other)
#block.call(other)
end
end.tap do |c|
c.instance_variable_set(:#block, block)
end
end
begin
raise "FOOBAR: We're all doomed!"
rescue exceptions_matching { |e| e.message =~ /^FOOBAR/ }
puts "rescued!"
end
If somebody knows when ruby removed lambda support in rescue please comment.
All classes under Errno are subclasses of SystemCallError. And all subclasses of SystemCallError are classes under Errno. The 2 sets are identical, so just rescue SystemCallError. This assumes that you're not using an external lib that adds to one and not the other.
Verify the identity of the 2 sets (using active_support):
Errno.constants.map {|name|
Errno.const_get(name)
}.select{|const|
Class === const
}.uniq.map(&:to_s).sort ==
SystemCallError.subclasses.map(&:to_s).sort
This returns true for me.
So, applied to your example:
begin
# my code
rescue SystemCallError
# handle exception
end
Here is a more generic solution, in the case you wanted to rescue some Errno types and not others.
Create a custom module to be included by all the error classes we want to rescue
module MyErrnoModule; end
Customize this array to your liking, up to the "each" call.
Errno.constants.map {|name|
Errno.const_get(name)
}.select{|const|
Class === const
}.uniq.each {|klass|
klass.class_eval {
include MyErrnoModule
}
}
Test:
begin
raise Errno::EPERM
rescue MyErrnoModule
p "rescued #{$!.inspect}"
end
Test result:
"rescued #<Errno::EPERM: Operation not permitted>"
I would guess this performs slightly better than a solution that needs to check the name of the exception.

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