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 Strategynot #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; endend
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; endend
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 NilClassraise 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; endend# Stub service representing 3rd party cloud storageclass 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 endenderrors_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 endendif defined?(pp) || require('pp') pp errors_mapelse errors_map.each{ |set| puts set.inspect }end