Ruby, an Exceptional Language

Based on the book Exceptional Ruby by Avdi Grimm, I have developed a strategy for how I should deal with exceptions in Ruby.

Being a very dynamic language, Ruby allows very flexible coding techniques. Exceptions are not an exception :).

When I am developing a library in Ruby I typically create one Error module and one StdError class. The Error module is a typical tag module and does not contain any methods.

Tag Module

# Tag module for the Tapir library
module Tapir
	module Error
	end
end

The reason for the tag module is that I can use it to tag exceptions
occurring inside my library without having to wrap them in a nested
exception.

module Tapir
class Downloader

	def self.get url
		HTTP.get url
			rescue StandardError -> error # Rescue the error
			# Namespace the error by tagging it with ::Tapir::Error
			error.extend(::Tapir::Error)
			raise # And raise it again
		end

	end
end

# Client usage
begin
	Tapir::Downloader.get 'http://non.existent.url/'
	rescue Tapir::Error => error
	puts "Stupid tapir, gave me error #{error.message}"
end

This is beautiful. I am scoping an internal error as my own. Since Ruby
is dynamic there is no need to declare a new class that wraps all the
methods in the StandarError I have access to them anyway. Duck typing
for the win!

A Nested Exception Class

In some cases the tag module is not enough. Perhaps the exception was
not created by another exception. In that case I need a real class since
it is not possible to raise modules. But while I am at it I usually make
the class a nested exception in order to simplify wrapping of other
exceptions if the need comes up. This is how I do that.

module Tapir
	# I usually call the class `StdError` since it prevents the user of
	# the library from rescuing the global `StandardError`.
	class StdError < StandardError
		extend Error # Extend the Error tag module
		attr_reader :original # Add an accessor for the original, if one exists

		# Create the error with a message and an original that defaults to
		# the exception that is currently active, in this thread, if one exists
		def initialize(msg, original=$!)
			super(msg)
			@original = original;
		end
	end
end

# Client Usage
begin
	Tapir.do_something_that_fails
	rescue Tapir::Error => error # rescue the tag module
	puts "Bad tapir #{error.message}, due to #{error.original.message}"
end

# or if I want to be more specific
begin
	Tapir.do_something_that_fails
	rescue Tapir::StdError => error # rescue the specific error
	puts "Bad tapir #{error.message}, due to #{error.original.message}"
end

Notice that I don’t have to wrap the exception explicitly, since
I default the Exception to the last error that is stored in $!.

Now the only reason for me to want to create a Tapir::StdError apart
from it being misuse of my library is if I want to add additional information
to the exception that already occurred. In that case I may also want to
extend the Tapir::StdError and create an exception with additional
fields.

module Tapir

	# Create a specific exception to add more information for the client
	class TooOldError < StdError
		attr_reader :age, :max_age

		def initialize(msg, original=$!, age, max_age)
			super(msg, original)
			@age, @max_age = age, max_age
		end
	end
end

# Client usage
begin
	tapir.mate(other_tapir)
	rescue TooOldError => error
	# Use the specific error properties
	puts "Hey, your are #{error.age}, that is too damn old!"
end

Throw – Catch

Ruby also has an alternative to raise and rescue called throw and
catch.

They should not be used as an alternative to exceptions, instead they are
escape continuations
that should be used to escape from nested control structures across
method calls. Powerful! Here is an example from Sinatra

# Here is the throw

# Pass control to the next matching route.
# If there are no more matching routes, Sinatra will
# return a 404 response.
def pass(&block)
	throw :pass, block
end

# and here is where it is caught

def process_route(pattern, keys, conditions)
	...
	catch(:pass) do
		conditions.each { |cond|
		throw :pass if instance_eval(&cond) == false }
		yield
	end
end

# Allowing usage such as

get '/guess/:who' do
	pass unless params[:who] == 'Frank'
	'You got me!'
end

get '/guess/*' do
	'You missed!'
end

Lovely!

Wrap up

This is how I use exceptions in Ruby now, thanks to ideas from the book.
Other good ideas from the book are the three guarantees:

  • The weak guarantee, if an exception is raised, the object will be in
    a consistent state.
  • The strong guarantee, if an exception is raised, the object will be
    left in its initial state.
  • The nothrow guarantee, no exceptions will be raised by this method.

And a nice way of categorizing exceptions based on three different
usages by the client. (My categories are not exactly the same as Avdis)

  • User Error, the client has used the library wrong.
  • Internal Error, something is wrong with the library. We are looking
    into the problem…
  • Transient Error, something is now working right now, but the same
    call may succeed in a while. It is a good idea to provide a period
    after whick the call will probably succeed.
    the client to try again.

It is a great book which contains a lot more information than I covered
here. Get it, it is well worth the money.

Leave a Reply

Close Menu