Summoning Error Classes As Needed

Posted over 3 years ago in Ruby Tutorials and Ruby Voodoo.

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().

Adam Keys added about 1 hour later:

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

StarTrader added about 2 hours later:

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?

James Whiteman added 1 day later:

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

Daniel Berger added about 1 month later:

This blog post from Jamis Buck may interest you:

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

Arya Asemanfar added 3 months later:

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

Add Your Thoughts

You can use Markdown in the body of your comment to format text and make links.

Note that I reserve the right to edit any content you post here. I typically exercise this right to fix formatting issues. All posts must be approved so spam will never be seen on these pages.

Author:
URL or Email (optional):
Body: