Reputation: 3455
I'm defining a custom Exception on a model in rails as kind of a wrapper Exception: (begin[code]rescue[raise custom exception]end
)
When I raise the Exception, I'd like to pass it some info about a) the instance of the model whose internal functions raise the error, and b) the error that was caught.
This is going on an automated import method of a model that gets populated by POST request to from foreign datasource.
tldr; How can one pass arguments to an Exception, given that you define the Exception yourself? I have an initialize method on that Exception but the raise
syntax seems to only accept an Exception class and message, no optional parameters that get passed into the instantiation process.
Upvotes: 63
Views: 81433
Reputation: 3758
class FooError < StandardError
attr_reader :foo
def initialize(foo)
super
@foo = foo
end
end
This is the best way if you follow the Rubocop Style Guide and always pass your message as the second argument to raise
:
raise FooError.new(foo), 'argh'
You can get foo
like this:
rescue FooError => error
error.foo # => 1234
error.message # => 'argh'
If you want to customize the error message then write:
class FooError < StandardError
attr_reader :foo
def initialize(foo)
super
@foo = foo
end
def message
"The foo is: #{foo}"
end
end
This works great if foo
is required. If you want foo
to be an optional argument, then keep reading.
And want this to work:
raise FooError.new('argh', foo)
You need to pass the message to super
as the only argument:
class FooError < StandardError
attr_reader :foo
def initialize(message, foo)
super(message)
@foo = foo
end
end
raise
As the Rubocop Style Guide says, the message and the exception should be passed separately. If you write:
raise FooError.new('argh')
And want to pass a backtrace, there is no way to do it without passing the message twice:
raise FooError.new('argh'), 'argh', other_error.backtrace
You need to pass a backtrace if you want to re-raise an exception as a new instance with the same backtrace and a different message or data. Sometimes this is very useful.
The crux of the problem is a design flaw in Ruby: exception messages get set in two different ways.
raise StandardError, 'argh' # case 1
raise StandardError.new('argh') # case 2
In case 1, raise
just calls StandardError.new('argh')
, so these are the same. But what if you pass an exception instance and a message to raise
?
raise FooError.new(foo), 'argh', backtrace
raise
will set 'argh'
as the message on the FooError
instance, so it behaves as if you called super('argh')
in FooError#initialize
.
We want to be able to use this syntax, because otherwise, we'll have to pass the message twice anytime we want to pass a backtrace:
raise FooError.new(foo, 'argh'), 'argh', backtrace
raise FooError.new('argh', foo), 'argh', backtrace
But what if foo
is optional? Then FooError#initialize
is overloaded.
raise FooError, 'argh' # case A
raise FooError.new(foo), 'argh' # case B
In case A, raise
will call FooError.new('argh')
, but your code expects an optional foo
, not a message. This is bad. What are your options?
accept that the value passed to FooError#initialize
may be either foo
or a message.
Don't use case A style. If you're not passing foo
, write raise FooError.new(), 'argh'
Make foo a keyword argument
IMO, don't do 2. The code's not self-documenting, so you have to remember all of this. Too complicated.
If you don't want to use a keyword argument, my implementation of FooError
way at the top of this answer actually works great with 1. This is why FooError#initialize
has to call super
and not super()
. Because when you write raise FooError, 'argh'
, foo
will be 'argh'
, and you have to pass it to the parent class to set the message. The code doesn't break if you call super
with something that isn't a string; nothing happens.
3 is the simplest option, if you're ok with a keyword argument - h/t Lemon Cat. Here's the code for that:
class FooError < StandardError
attr_reader :foo
def initialize(message, foo: nil)
super(message)
@foo = foo
end
end
raise FooError, 'message', backtrace
raise FooError(foo: foo), 'message', backtrace
Upvotes: 42
Reputation: 51113
It's counterintuitive for programmers coming from e.g. Java, but the most effective way to do this is not to write a custom initializer, but rather to write your own replacement for the Exception::exception
class method.
Per the Kernel#raise
docs:
the first parameter should be an Exception class (or another object that returns an
Exception
object when sent anexception
message). [Emphasis added.]
class MyException < StandardError
class << self
def exception(arg)
# per `Exception::exception` docs
return self if arg.nil? || self.equal?(arg)
return MyException.new(arg.to_s) unless arg.is_a?(MyModel)
# $! is a magic global variable holding the last raised
# exception; Kernel#raise will also inject it as the
# cause attribute of the exception we construct here
error_caught = $!
msg = custom_message_for(arg, error_caught)
ex = MyException.new(msg)
# … any additional initialization goes here
ex
end
private
def custom_message_for(my_model_instance, error_caught)
# …
end
end
end
This way, you can raise your custom exception normally, with a model instance instead of a string message, without having to remember to call new
explicitly and upset RuboCop, as well as confusing Ruby programmers that come to your code later expecting the standard syntax.
begin
my_model.frob
rescue => e
raise MyException, my_model # works
end
raise MyException, 'some other reason' # also works
The message & initialization logic from MyException#exception
could also go in a custom initializer, letting you just write MyException.new(arg, $!)
, but in that case make sure the initializer is smart enough to also handle a plain string message, and make sure it at some point calls super
with a string message.
Upvotes: -1
Reputation: 6320
If the extra information you're looking to pass is simply a type with a message, this works well:
# define custom error class
class MyCustomError < StandardError; end
# raise error with extra information
raise MyCustomError, 'Extra Information'
The result (in IRB):
Traceback (most recent call last):
2: from (irb):22
1: from (irb):22:in `rescue in irb_binding'
MyCustomError (Extra Information)
The pattern below has become exceptionally useful for me (pun intended). It's clean, can be easily modularized, and the errors are expressive. Within my class I define new errors that inherit from StandardError
, and I raise them with messages (for example, the object associated with the error).
Here's a simple example, similar to OP's original question, that raises a custom error within a class and captures the method name in the error message:
class MyUser
# class errors
class MyUserInitializationError < StandardError; end
# instance methods
def simulate_failure
raise MyUserInitializationError, "method failed: #{__method__}"
end
end
# example usage:
MyUser.new.simulate_failure
# => MyUser::MyUserInitializationError (method failed: simulate_failure)
Upvotes: 1
Reputation: 1152
TL;DR 7 years after this question, I believe the correct answer is:
class CustomException < StandardError
attr_reader :extra
def initialize(message=nil, extra: nil)
super(message)
@extra = extra
end
end
# => nil
raise CustomException.new('some message', extra: "blupp")
WARNING: you will get identical results with:
raise CustomException.new(extra: 'blupp'), 'some message'
but that is because Exception#exception(string)
does a #rb_obj_clone
on self
, and then calls exc_initialize
(which does NOT call CustomException#initialize
. From error.c:
static VALUE
exc_exception(int argc, VALUE *argv, VALUE self)
{
VALUE exc;
if (argc == 0) return self;
if (argc == 1 && self == argv[0]) return self;
exc = rb_obj_clone(self);
exc_initialize(argc, argv, exc);
return exc;
}
In the latter example of #raise
up above, a CustomException
will be raise
d with message
set to "a message" and extra
set to "blupp" (because it is a clone) but TWO CustomException
objects are actually created: the first by CustomException.new
, and the second by #raise
calling #exception
on the first instance of CustomException
which creates a second cloned CustomException
.
My extended dance remix version of why is at: https://stackoverflow.com/a/56371923/5299483
Upvotes: 8
Reputation: 2248
Here is a sample code adding a code to an error:
class MyCustomError < StandardError
attr_reader :code
def initialize(code)
@code = code
end
def to_s
"[#{code}] #{super}"
end
end
And to raise it:
raise MyCustomError.new(code), message
Upvotes: 9
Reputation: 18835
create an instance of your exception with new:
class CustomException < StandardError
def initialize(data)
@data = data
end
end
# => nil
raise CustomException.new(bla: "blupp")
# CustomException: CustomException
Upvotes: 82
Reputation: 18193
You can create an new instance of your Exception
subclass, then raise that. For instance:
begin
# do something
rescue => e
error = MyException.new(e, 'some info')
raise error
end
Upvotes: -2