Ruby custom error classes: inheritance of the message attribute
raise
already sets the message so you don't have to pass it to the constructor:
class MyCustomError < StandardError
attr_reader :object
def initialize(object)
@object = object
end
end
begin
raise MyCustomError.new("an object"), "a message"
rescue MyCustomError => e
puts e.message # => "a message"
puts e.object # => "an object"
end
I've replaced rescue Exception
with rescue MyCustomError
, see Why is it a bad style to `rescue Exception => e` in Ruby?.
How to use custom error messages
Simply modify your script like this.
class MyCustomError < StandardError
def message
"oops... That was not a number"
end
end
def print_a_number
begin
puts "chose a number"
number = Float(gets) rescue false
raise MyCustomError unless number.is_a? Numeric
puts "The number you chose is #{number}"
rescue MyCustomError => err
puts err.message
puts err.backtrace.inspect
end
end
Ruby exception inheritance with dynamically generated classes
Ok, I'll try to help here :
First a module is not a class, it allows you to mix behaviour in a class. second see the following example :
module A
module B
module Error
def foobar
puts "foo"
end
end
end
end
class StandardError
include A::B::Error
end
StandardError.new.kind_of?(A::B::Error)
StandardError.new.kind_of?(A::B)
StandardError.included_modules #=> [A::B::Error,Kernel]
kind_of? tells you that yes, Error does possess All of A::B::Error behaviour (which is normal since it includes A::B::Error) however it does not include all the behaviour from A::B and therefore is not of the A::B kind. (duck typing)
Now there is a very good chance that ruby-aws reopens one of the superclass of NameError and includes Amazon::AWS:Error in there. (monkey patching)
You can find out programatically where the module is included in the hierarchy with the following :
class Class
def has_module?(module_ref)
if self.included_modules.include?(module_ref) and not self.superclass.included_modules.include?(module_ref)
puts self.name+" has module "+ module_ref.name
else
self.superclass.nil? ? false : self.superclass.has_module?(module_ref)
end
end
end
StandardError.has_module?(A::B::Error)
NameError.has_module?(A::B::Error)
Regarding your second question I can't see anything better than
begin
#do AWS error prone stuff
rescue Exception => e
if Amazon::AWS::Error.constants.include?(e.class.name)
#awsError
else
whatever
end
end
(edit -- above code doesn't work as is : name includes module prefix which is not the case of the constants arrays. You should definitely contact the lib maintainer the AWSError class looks more like a factory class to me :/ )
I don't have ruby-aws here and the caliban site is blocked by the company's firewall so I can't test much further.
Regarding the include : that might be the thing doing the monkey patching on the StandardError hierarchy. I am not sure anymore but most likely doing it at the root of a file outside every context is including the module on Object or on the Object metaclass. (this is what would happen in IRB, where the default context is Object, not sure about in a file)
from the pickaxe on modules :
A couple of points about the include statement before we go on. First, it has nothing to do with files. C programmers use a preprocessor directive called #include to insert the contents of one file into another during compilation. The Ruby include statement simply makes a reference to a named module. If that module is in a separate file, you must use require to drag that file in before using include.
(edit -- I can't seem to be able to comment using this browser :/ yay for locked in platforms)
Raising an an exception: string vs custom class
There is an important advantage of custom errors like ApiTokenExpired
if you have to handle the error in an specific way somewhere in the call stack.
begin
# ...
rescue ApiTokenExpired => error
# handle the specific error
rescue => error
# default error handling
end
IMO the decision to create custom errors doesn't depend on the size of the project or future maintenance.
Since custom error classes cause an extra effort I don't use it by default. When a special error handling is needed then an error class should be introduced.
Raise custom Exception with arguments
Solution:
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.
Explanation:
Pass your message as the second argument to 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.
Implementing 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
Getting a list of existing Rails Error classes for re-use/inheritance
There's a mostly adequate solution here: http://www.ruby-forum.com/topic/158088
Since this question is unanswered, yet coming up at the top of Google search results, I decided to wrap Frederick Cheung's solution up in a rake task and post it here.
Drop the following in lib/tasks/exceptions.rake
namespace :exceptions do
task :list => :environment do
exceptions = []
ObjectSpace.each_object(Class) do |k|
exceptions << k if k.ancestors.include?(Exception)
end
puts exceptions.sort { |a,b| a.to_s <=> b.to_s }.join("\n")
end
end
Run it with:
bundle exec rake exceptions:list
If you're still on Rails 2, or not using Bundler, leave off the bundle exec
This list is probably adequate, but not exhaustive. For example, ActiveResource defines several exceptions such as ActiveResource::ConnectionError
and ActiveResource::TimeoutError
that are not appearing when I run this task. Maybe someone else can enlighten me as to why.
Ruby's Exception Error classes
Define an initialize method, which takes the message as an argument with a default value. Then call StandardError
's initialize method with that message (using super
).
class MyError < StandardError
def initialize(msg = "You've triggered a MyError")
super(msg)
end
end
Make error show conditional message?
where you have
<% if object.errors.count = 1 %>
you need to have:
<% if object.errors.count == 1 %>
note the double equal sign.
Related Topics
How to Make a Post Request with Open-Uri
Get Sidekiq to Execute a Job Immediately
Ruby Timeouts and System Commands
How to Calculate How Many Years Passed Since a Given Date in Ruby
Notices for Sequence After Running Migration in Rails on Postgresql Application
Homebrew Installation on MAC Os X Failed to Connect to Raw.Githubusercontent.Com Port 443
Rails Form Validation Conditional Bypass
How to Express Infinity in Ruby
Differencebetween 'Try' and '&.' (Safe Navigation Operator) in Ruby
Create Custom HTML Helpers in Ruby on Rails
What Does "Shadowing" Mean in Ruby
How to Solve Insecure World Writable Dir /Usr in Path,Mode 040777 Warning on Ruby
How to Get a Backtrace from a Systemstackerror: Stack Level Too Deep
What Does "<Top (Required)>" Mean in a Ruby Stack Trace