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.
Creating Custom Exceptions Most of the built-in exceptions are also derived from this class. Here, we have created a user-defined exception called CustomError which inherits from the Exception class. This new exception, like other exceptions, can be raised using the raise statement with an optional error message.
except ExceptionType, Argument: You can print value of Argument here... If you write the code to handle a single exception, you can have a variable follow the name of the exception in the except statement. If you are trapping multiple exceptions, you can have a variable follow the tuple of the exception.
ArgumentException is thrown when a method is invoked and at least one of the passed arguments does not meet the parameter specification of the called method. The ParamName property identifies the invalid argument.
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
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'), 'bar'
You can get foo
like this:
rescue FooError => error error.foo # => 'foo' error.message # => 'bar'
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 well if foo
is required. If you want foo
to be an optional argument, then keep reading.
raise
As the Rubocop Style Guide says, the message and the exception class should be provided as separate arguments because if you write:
raise FooError.new('bar')
And want to pass a backtrace to raise
, there is no way to do it without passing the message twice:
raise FooError.new('bar'), 'bar', other_error.backtrace
As this answer says, you will 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.
FooError
The crux of the problem is that if foo
is an optional argument, there are two different ways of raising exceptions:
raise FooError.new('foo'), 'bar', backtrace # case 1
and
raise FooError, 'bar', backtrace # case 2
and we want FooError
to work with both.
In case 1, since you've provided an error instance rather than a class, raise
sets 'bar'
as the message of the error instance.
In case 2, raise
instantiates FooError
for you and passes 'bar'
as the only argument, but it does not set the message after initialization like in case 1. To set the message, you have to call super
in FooError#initialize
with the message as the only argument.
So in case 1, FooError#initialize
receives 'foo'
, and in case 2, it receives 'bar'
. It's overloaded and there is no way in general to differentiate between these cases. This is a design flaw in Ruby. So if foo
is an optional argument, you have three choices:
(a) accept that the value passed to FooError#initialize
may be either foo
or a message.
(b) Use only case 1 or case 2 style with raise
but not both.
(c) Make foo
a keyword argument.
If you don't want foo
to be a keyword argument, I recommend (a) and my implementation of FooError
above is designed to work that way.
If you raise
a FooError
using case 2 style, the value of foo
is the message, which gets implicitly passed to super
. You will need an explicit super(foo)
if you add more arguments to FooError#initialize
.
If you use a keyword argument (h/t Lemon Cat's answer) then the code looks like:
class FooError < StandardError attr_reader :foo def initialize(message, foo: nil) super(message) @foo = foo end end
And raising looks like:
raise FooError, 'bar', backtrace raise FooError(foo: 'foo'), 'bar', backtrace
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With