Ruby custom error classes: inheritance of the message attribute

69,242

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.

Share:
69,242
MarioDS
Author by

MarioDS

Updated on July 08, 2022

Comments

  • MarioDS
    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 be rescued:

    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
    MarioDS about 11 years
    Okay, I thought that the message you gave was a second parameter to the raise keyword or something.
  • sawa
    sawa about 11 years
    You redefined initialize to take two arguments. new passes the arguments to initialize.
  • sawa
    sawa about 11 years
    Or, you can omit the parentheses.
  • MarioDS
    MarioDS about 11 years
    I 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 calls raise with two parameters: a new BillRowError object, and his message. I'm just confused by the syntax... On other tutorials I always see it like this: raise Error, message
  • sawa
    sawa about 11 years
    The problem is not with how many arguments you pass to raise; that is pretty much flexible. The problem is that you defined initialize to take two arguments and only gave one. Look in your example. BillRowError.new(:roamingcalls, @index) is given two arguments.
  • MarioDS
    MarioDS about 11 years
    Indeed, 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 the BillRowError object? Sorry if this discussion annoys you, thanks for your explanation so far.
  • sawa
    sawa about 11 years
    I don't know. You can try it.
  • MarioDS
    MarioDS about 11 years
    I'll accept your answer because you showed me the entire syntax. Thanks!
  • Dfr
    Dfr almost 11 years
    Here we doing rescue Exception, but why not rescue MyCustomError?
  • hiroshi
    hiroshi almost 9 years
    FYI, if the first argument, object, is an option and raise MyCustomError, "a message" without new, "a message" will not be set.
  • CyberMew
    CyberMew almost 6 years
    Is there a way to get the raised message in our custom exception class somehow?
  • Stefan
    Stefan almost 6 years
    @CyberMew what do you mean? What do you want to do?
  • CyberMew
    CyberMew almost 6 years
    I 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
    Stefan almost 6 years
    @CyberMew could you post a separate question, please? Sounds interesting.
  • CyberMew
    CyberMew almost 6 years
    Done, I asked it over here on a new question: stackoverflow.com/questions/50503673/…