On the Ruby Best Practices blog, Gregory Brown wrote an interesting article on a quick way for making exception classes. I do believe, though, that we can take what he has done a bit further.
Exception classes are useful when a class must throw exceptions on several different conditions and the code that catches these exceptions must be able to tell the difference between them. For example, an HTTP client class might throw HTTP404 (not found) or HTTP500 (internal server error). The code that catches these exceptions would do different things based on the exception. For an HTTP500, the server might be having a temporary problem, so the client should try again later. For an HTTP404, the resource is not found, and the client should not try again.
The most obvious way of implementing these different actions is to subclass the StandardError class directly. However, this is quite verbose and it add considerable bulk to otherwise trivial code.
#!/usr/bin/env ruby
class HTTPClient
class HTTP404 < StandardError; end
class HTTP500 < StandardError; end
end
Brown shows you how to use Class.new and anonymous classes. This is interesting, and it gets the job done, but it's not as Rubyish as it could be. Even if you wrap it up in a loop, defining your exceptions as a list, it can still be better.
#!/usr/bin/env ruby
class HTTPClient
const_set( "HTTP404", Class.new(StandardError) )
const_set( "HTTP500", Class.new(StandardError) )
end
The following code implements an exceptions helper, just like attr_reader or attr_accessor. It's implemented by defining a method of the class Module. This is the context Ruby is in when you're defining a class; you can define other such methods this way. The method makes use of class_eval. This is a bit of a hack, but it's the easiest way I know of to define new classes in the class context from the module.
#!/usr/bin/env ruby
class Module
def exceptions(*names)
names.each do|n|
# Clean up the name
n = n.to_s.capitalize
class_eval %{
# Define a common Error class if it's
# not yet defined
unless const_defined?("Error")
const_set("Error", Class.new(StandardError) )
end
# Define the exception class
class #{n} < Error; end
}
end
end
end
This code has some nice features. First, it'll automatically define an Error class for each class that uses exceptions from this helper. For example, the following two classes each have Error classes. There is an HTTPClient::Error class and a Monkey::Error class. This means you can either try to catch StandardExceptions (which will catch everything), a class specific error class (which will catch any exceptions for that class), or a specific exception class (such as HTTPError::HTTP404, which will only catch that exception type). This gives you great control over how you can catch the exceptions created by this helper.
The following (silly) example illustrates this. The Monkey won't dance if he's hungry or tired. The code that catches the exceptions thrown by the dance method can catch the hungry condition, or any generic Monkey::Error condition.
#!/usr/bin/env ruby
require 'exceptions.rb'
class Monkey
exceptions :hungry, :tired
def initialize
# Randomly select hungry or tired
@state = %w{ hungry tired }.sort_by{rand}[0]
end
def dance
case @state
when "hungry"
raise Hungry
when "tired"
raise Tired
end
puts "Monkey is happy, he will dance!"
end
end
begin
steve = Monkey.new
steve.dance
rescue Monkey::Hungry
puts "Monkey is hungry, he will not dance"
rescue Monkey::Error
puts "Something else is wrong with the monkey"
end

