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 Exc
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)
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
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
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
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