Ruby custom error classes: inheritance of the message attribute
Solution 1
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?.
Solution 2
Given what the ruby core documentation of Exception
, from which all other errors inherit, states about #message
Returns the result of invoking exception.to_s. Normally this returns the exception’s message or name. By supplying a to_str method, exceptions are agreeing to be used where Strings are expected.
http://ruby-doc.org/core-1.9.3/Exception.html#method-i-message
I would opt for redefining to_s
/to_str
or the initializer. Here is an example where we want to know, in a mostly human readable way, when an external service has failed to do something.
NOTE: The second strategy below uses the rails pretty string methods, such as demodualize
, which may be a little complicated and therefore potentially unwise to do in an exception. You could also add more arguments to the method signature, should you need.
Overriding #to_s Strategy not #to_str, it works differently
module ExternalService
class FailedCRUDError < ::StandardError
def to_s
'failed to crud with external service'
end
end
class FailedToCreateError < FailedCRUDError; end
class FailedToReadError < FailedCRUDError; end
class FailedToUpdateError < FailedCRUDError; end
class FailedToDeleteError < FailedCRUDError; end
end
Console Output
begin; raise ExternalService::FailedToCreateError; rescue => e; e.message; end
# => "failed to crud with external service"
begin; raise ExternalService::FailedToCreateError, 'custom message'; rescue => e; e.message; end
# => "failed to crud with external service"
begin; raise ExternalService::FailedToCreateError.new('custom message'); rescue => e; e.message; end
# => "failed to crud with external service"
raise ExternalService::FailedToCreateError
# ExternalService::FailedToCreateError: failed to crud with external service
Overriding #initialize Strategy
This is the strategy closest to implementations I've used in rails. As noted above, it uses the demodualize
, underscore
, and humanize
ActiveSupport
methods. But this could be easily removed, as in the previous strategy.
module ExternalService
class FailedCRUDError < ::StandardError
def initialize(service_model=nil)
super("#{self.class.name.demodulize.underscore.humanize} using #{service_model.class}")
end
end
class FailedToCreateError < FailedCRUDError; end
class FailedToReadError < FailedCRUDError; end
class FailedToUpdateError < FailedCRUDError; end
class FailedToDeleteError < FailedCRUDError; end
end
Console Output
begin; raise ExternalService::FailedToCreateError; rescue => e; e.message; end
# => "Failed to create error using NilClass"
begin; raise ExternalService::FailedToCreateError, Object.new; rescue => e; e.message; end
# => "Failed to create error using Object"
begin; raise ExternalService::FailedToCreateError.new(Object.new); rescue => e; e.message; end
# => "Failed to create error using Object"
raise ExternalService::FailedCRUDError
# ExternalService::FailedCRUDError: Failed crud error using NilClass
raise ExternalService::FailedCRUDError.new(Object.new)
# RuntimeError: ExternalService::FailedCRUDError using Object
Demo Tool
This is a demo to show rescuing and messaging of the above implementation. The class raising the exceptions is a fake API to Cloudinary. Just dump one of the above strategies into your rails console, followed by this.
require 'rails' # only needed for second strategy
module ExternalService
class FailedCRUDError < ::StandardError
def initialize(service_model=nil)
@service_model = service_model
super("#{self.class.name.demodulize.underscore.humanize} using #{@service_model.class}")
end
end
class FailedToCreateError < FailedCRUDError; end
class FailedToReadError < FailedCRUDError; end
class FailedToUpdateError < FailedCRUDError; end
class FailedToDeleteError < FailedCRUDError; end
end
# Stub service representing 3rd party cloud storage
class Cloudinary
def initialize(*error_args)
@error_args = error_args.flatten
end
def create_read_update_or_delete
begin
try_and_fail
rescue ExternalService::FailedCRUDError => e
e.message
end
end
private def try_and_fail
raise *@error_args
end
end
errors_map = [
# Without an arg
ExternalService::FailedCRUDError,
ExternalService::FailedToCreateError,
ExternalService::FailedToReadError,
ExternalService::FailedToUpdateError,
ExternalService::FailedToDeleteError,
# Instantiated without an arg
ExternalService::FailedCRUDError.new,
ExternalService::FailedToCreateError.new,
ExternalService::FailedToReadError.new,
ExternalService::FailedToUpdateError.new,
ExternalService::FailedToDeleteError.new,
# With an arg
[ExternalService::FailedCRUDError, Object.new],
[ExternalService::FailedToCreateError, Object.new],
[ExternalService::FailedToReadError, Object.new],
[ExternalService::FailedToUpdateError, Object.new],
[ExternalService::FailedToDeleteError, Object.new],
# Instantiated with an arg
ExternalService::FailedCRUDError.new(Object.new),
ExternalService::FailedToCreateError.new(Object.new),
ExternalService::FailedToReadError.new(Object.new),
ExternalService::FailedToUpdateError.new(Object.new),
ExternalService::FailedToDeleteError.new(Object.new),
].inject({}) do |errors, args|
begin
errors.merge!( args => Cloudinary.new(args).create_read_update_or_delete)
rescue => e
binding.pry
end
end
if defined?(pp) || require('pp')
pp errors_map
else
errors_map.each{ |set| puts set.inspect }
end
Solution 3
Your idea is right, but the way you call it is wrong. It should be
raise MyCustomError.new(an_object, "A message")
Solution 4
I wanted to do something similar. I wanted to pass an object to #new and have the message set based on some processing of the passed object. The following works.
class FooError < StandardError
attr_accessor :message # this is critical!
def initialize(stuff)
@message = stuff.reverse
end
end
begin
raise FooError.new("!dlroW olleH")
rescue FooError => e
puts e.message #=> Hello World!
end
Note that if you don't declare attr_accessor :message
then it will not work. Addressing the OP's issue, you could also pass the message as an additional argument and store anything you like. The crucial part appears to be overriding #message.
MarioDS
Updated on July 08, 2022Comments
-
MarioDS almost 2 years
I can't seem to find much information about custom exception classes.
What I do know
You can declare your custom error class and let it inherit from
StandardError
, so it can berescue
d:class MyCustomError < StandardError end
This allows you to raise it using:
raise MyCustomError, "A message"
and later, get that message when rescuing
rescue MyCustomError => e puts e.message # => "A message"
What I don't know
I want to give my exception some custom fields, but I want to inherit the
message
attribute from the parent class. I found out reading on this topic that@message
is not an instance variable of the exception class, so I'm worried that my inheritance won't work.Can anyone give me more details to this? How would I implement a custom error class with an
object
attribute? Is the following correct:class MyCustomError < StandardError attr_reader :object def initialize(message, object) super(message) @object = object end end
And then:
raise MyCustomError.new(anObject), "A message"
to get:
rescue MyCustomError => e puts e.message # => "A message" puts e.object # => anObject
will it work, and if it does, is this the correct way of doing things?
-
MarioDS about 11 yearsOkay, I thought that the message you gave was a second parameter to the
raise
keyword or something. -
sawa about 11 yearsYou redefined
initialize
to take two arguments.new
passes the arguments toinitialize
. -
sawa about 11 yearsOr, you can omit the parentheses.
-
MarioDS about 11 yearsI understand that bit, but the poster of the topic I linked to in my question does it like this:
raise(BillRowError.new(:roamingcalls, @index), "Roaming Calls field missing")
. So he callsraise
with two parameters: a newBillRowError
object, and his message. I'm just confused by the syntax... On other tutorials I always see it like this:raise Error, message
-
sawa about 11 yearsThe problem is not with how many arguments you pass to
raise
; that is pretty much flexible. The problem is that you definedinitialize
to take two arguments and only gave one. Look in your example.BillRowError.new(:roamingcalls, @index)
is given two arguments. -
MarioDS about 11 yearsIndeed, it is, but the message is set as well. But I wonder: is the second argument to
raise
, in this example the message, also set as the message on theBillRowError
object? Sorry if this discussion annoys you, thanks for your explanation so far. -
sawa about 11 yearsI don't know. You can try it.
-
MarioDS about 11 yearsI'll accept your answer because you showed me the entire syntax. Thanks!
-
Dfr almost 11 yearsHere we doing
rescue Exception
, but why notrescue MyCustomError
? -
hiroshi almost 9 yearsFYI, if the first argument, object, is an option and
raise MyCustomError, "a message"
withoutnew
, "a message" will not be set. -
CyberMew almost 6 yearsIs there a way to get the raised message in our custom exception class somehow?
-
Stefan almost 6 years@CyberMew what do you mean? What do you want to do?
-
CyberMew almost 6 yearsI want the custom class to be able to get the raise error message as well if possible? 'message' doesn't seem to work though
-
Stefan almost 6 years@CyberMew could you post a separate question, please? Sounds interesting.
-
CyberMew almost 6 yearsDone, I asked it over here on a new question: stackoverflow.com/questions/50503673/…