Ruby Voodoo

Deep dives into random corners of my favorite programming language.

26

JUN
2008

Summoning Error Classes As Needed

In the past, I've written a lot of code like this:

class SomeThing
  class SomeError       < RuntimeError; end
  class AnotherError    < RuntimeError; end
  class YetAnotherError < RuntimeError; end

  # some methods that can raise the above errors...
end

I have a new strategy I've been using for code like this and it has really been working out well. Here is how I do the same thing today:

class SmarterThing
  def self.const_missing(error_name)  # :nodoc:
    if error_name.to_s =~ /Error\z/
      const_set(error_name, Class.new(RuntimeError))
    else
      super
    end
  end

  # error raising methods here...
end

Let's discuss how this works. The const_missing() method is a hook in Ruby, much like the beloved method_missing(). Note that const_missing() is a class method though, instead of an instance method. When a constant is used and Ruby can't find it, a call to the hook will be triggered.

In this version, I just check to see if the constant name ends in Error. If it does, I summon an Exception subclass on the fly. Other calls to this hook are forwarded on to Ruby's default error raising implementation via super.

The building of the Exception subclass has a few interesting points of note. First, we see that we can build a Class object as we do any other with a simple call to new(). Beyond that, we can pass new() a parent Class we would like to inherit from. So if you would prefer to inherit from StandardError, you can just change the reference here. Finally, const_set() assigns the new Class to the constant name referenced and returns it. This means that future references for the same constant will not go through this hook and will receive the same Class.

That's the how, but let's talk a little about the why.

When I showed this trick to a friend, he complained that these summoned errors are not easily RDoced. That's true, but I've actually found this is improving my documentation instead of hurting it.

Raise your hand if you tend to click on all the errors listed in the API documentation and read about those. Yeah, I don't either. With those gone (and note that I explicitly disabled RDoc for my hack), I just add details about the Exceptions a method can raise to the RDoc of that method. The end result of all this is that the documentation has moved to a place where it is helpful to me and thus I actually read it.

A final bonus of this technique is that it even works if you are dynamically generating error names in some code, say with const_get().

Comments (5)
  1. Adam Keys
    Adam Keys June 26th, 2008 Reply Link

    A trick that brings my favorite method, Class.new, together with my favorite family of methods, const_*? Good show!

    1. Reply (using GitHub Flavored Markdown)

      Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

      Ajax loader
  2. StarTrader
    StarTrader June 26th, 2008 Reply Link

    I like the implementation as a concept, but I am concerned about what the results would be for client code. This would seem to encourage the creation of an arbitrarily large number of Error classes. To my mind the goal of raising an error is to have some other part of the program catch it and do something intelligent to fix it. I, therefore, only create a new error class when I think the error can be handled somewhere. This being the case, I find a list of all potentially handleable errors useful, even if I don't click on them in the rdocs.

    I guess my question is, why not just raise a RuntimeError or some other already defined Error class and use the properties of the Error class to give more detail?

    1. Reply (using GitHub Flavored Markdown)

      Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

      Ajax loader
    2. James Whiteman
      James Whiteman June 28th, 2008 Reply Link

      StarTrader—const_set means that the newly generated Error classes won't remain anonymous; you'd be able to rescue them by name.

      1. Reply (using GitHub Flavored Markdown)

        Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

        Ajax loader
    3. Daniel Berger
      Daniel Berger August 21st, 2008 Reply Link

      This blog post from Jamis Buck may interest you:

      http://weblog.jamisbuck.org/2007/3/7/raising-the-right-exception

      1. Reply (using GitHub Flavored Markdown)

        Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

        Ajax loader
  3. Arya Asemanfar
    Arya Asemanfar September 25th, 2008 Reply Link

    Also note that typo's won't be caught, causing more subclasses of RuntimeError to be created.

    Example:

    def foo
      # ... some code ...
      raise InvalidGidgetError
    end
    
    def bar
      # ...
      raise InvlaidGidgetError
    end
    

    now you have two exceptions.

    Maybe something like this would be more suitable, although not as concise.

    def create_empty_exceptions(*args) # or whatever else you'd like to call it
      args.each do |exception_class|
        # same code as your example to create classes and a set them to a constant
      end
    end
    
    1. Reply (using GitHub Flavored Markdown)

      Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

      Ajax loader
Leave a Comment (using GitHub Flavored Markdown)

Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

Ajax loader