1. Computing

Ruby Best Practices--Making Quick Exceptions

Implementing Exception Helpers

By

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

©2014 About.com. All rights reserved.