Exceptions in Ruby

I like Exceptions. They’re a very useful concept for raising and handling errors at the correct level without cluttering our code. Coming from C# the way Exceptions work in Ruby are intuitive to me, but there are actually a few extra features which aren’t available in the .NET world.

Basics First

To illustrate Exceptions, I’ve created a very simple class that simulates interaction with a remote resource.

class RemoteResource
  
  def initialize
    @users = Hash.new
    @users["Malcolm"] = "password1"
    @users["Angus"] = "password1"
    @users["Phil"] = "password1"
    @users["Cliff"] = "password1"
    @users["Brian"] = "password1"
  end

  def create_connection(username,password,secure=false)
    unless @users[username] && @users[username] == password 
      raise ArgumentError.new("Invalid username or password") 
    end
    raise SecurityError.new("Secure connections are unavailable") if secure
    @connection_initialized = true
  end
  
  def get_resource
    unless @connection_initialized
      raise SecurityError.new("A connection has not been established") 
    end
    raise StandardError.new("An unknown error has occurred") if rand(3) == 0
    "Widget #{rand(11)}"
  end

end

Let’s see what happens when we raise a simple exception.

remote_resource = RemoteResource.new
remote_resource.create_connection("Kurt","password1")

Terminal Output

As you would probably expect, our little application exits immediately and reports the type and message of the exception that was raised. So far so good.

Handling Exceptions

Let’s see how we handle an exception.

remote_resource = RemoteResource.new
begin
  remote_resource.create_connection("Kurt","password1")
rescue Exception => error
  print "Could not create connection: #{error}\n"
end

Pretty neat. This is probably not very good code, since we’re handling the global Exception class which is bad practice – we should try to handle only specific Exception classes.

remote_resource = RemoteResource.new
begin
  remote_resource.create_connection("Malcolm","password1", true)
rescue ArgumentError => argument_error
  print "Could not create connection: #{argument_error}\n"
rescue SecurityError => security_error
  print "Could not create secure connection: #{security_error}\n"
end

Terminal Output

We can also handle multiple Exception classes in one block.

remote_resource = RemoteResource.new
begin
  remote_resource.create_connection("Malcolm","password1", true)
rescue ArgumentError, SecurityError => error
  print "Could not create connection: #{error}\n"
end

Ensure code is executed

We can also ensure that a block of code is executed – for example, we might want to add some logging code around the accessing of the remote resource.

remote_resource = RemoteResource.new
remote_resource.create_connection("Malcolm","password1")
print "Connection created at #{Time.now}\n"
begin
  result = remote_resource.get_resource
rescue SecurityError => error
  print "Error using connection: #{error}\n"
ensure
  print "Resource released at #{Time.now}\n"
end

Terminal Output

As the name implies, ensure ensures that a certain block of code is executed – as you can see from the output. C# users will immediately recognize a strong similarity to the finally keyword in .NET.

Ruby also introduces the else clause – this will only execute if no errors were encountered. The else clause must go before the ensure clause and after any rescue clauses.

remote_resource = RemoteResource.new
remote_resource.create_connection("Angus","password1")
print "Connection created at #{Time.now}\n"
begin
  result = remote_resource.get_resource
rescue SecurityError => error
  print "Error using connection: #{error}\n"
else 
  print "The connection was accessed without any errors\n"
ensure
  print "Resource released at #{Time.now}\n"
end

This is probably quite useful in certain scenarios.

Retry the block

Ruby also allows us to correct the cause of the exception and retry the block. For example, since my remote resource is a little unstable I might choose to retry the block until the resource is returned.

remote_resource = RemoteResource.new
remote_resource.create_connection("Angus","password1")
begin
  result = remote_resource.get_resource
  print "#{result}\n"
rescue SecurityError => error
  print "Error using connection: #{error}\n"
rescue StandardError => error
  print "Error accessing remote resource - #{error}\n"
  retry
end

Terminal Output

As you might imagine, it’s one of the easiest ways of creating an infinite loop so use with care. In fact, I actually managed to create an infinite loop while writing this post!

Conclusion

As someone coming from C# the Exception handling in Ruby seems very intuitive to me. The additional functionality (else and retry) could also be very useful in certain scenarios.

Happy coding.