Ruby 自定义错误类: message 属性的继承

我似乎找不到太多关于自定义异常类的信息。

我只知道

您可以声明您的自定义错误类并让它从 StandardError继承,因此它可以是 rescued:

class MyCustomError < StandardError
end

这样你就可以使用:

raise MyCustomError, "A message"

之后,在营救的时候得到这个信息

rescue MyCustomError => e
puts e.message # => "A message"

我不知道的是

我想为异常提供一些自定义字段,但是我想从父类继承 message属性。我在阅读 关于这个话题时发现,@message不是异常类的实例变量,所以我担心我的继承不会起作用。

有人能告诉我更多细节吗?如何实现具有 object属性的自定义错误类?以下内容是否正确:

class MyCustomError < StandardError
attr_reader :object
def initialize(message, object)
super(message)
@object = object
end
end

然后:

raise MyCustomError.new(anObject), "A message"

获得:

rescue MyCustomError => e
puts e.message # => "A message"
puts e.object # => anObject

它会起作用吗? 如果起作用了,这是正确的做事方式吗?

71869 次浏览

Your idea is right, but the way you call it is wrong. It should be

raise MyCustomError.new(an_object, "A message")

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?.

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.

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