Exceptions#

An exception happens when some code breaks the program flow due to unexpected circumstances. In this case, program creates the Exception object and pass the control to exception handler or - if it is not handled - breaks execution of the program. The reasons for the exception can be inside the program (like, for example, division by zero) or outside (file does not exists, can not connect to the database). To have an example consider the small code below which tries to print the content of the file given as an argument to stdout - exactly like cat command does:

def cat(file)
  f = open(file)
  puts f.read
  f.close
  puts "### end of file: #{file} ###"
end
cat ARGV[0]
puts "### end of program ###"

In normal circumstances this scripts does what it should do. It is reading the given file and printing it out to stdout:

joe:~ turbo$ ruby cat.rb /etc/ftpusers
# list of users disallowed any ftp access.
...
www
### end of file: /etc/ftpusers ###
### end of program ###

Exception Raised#

But what if you make a typo and will try to read not existing file? Then function open raises an exception, program stops the flow and the stack trace is printed to stdout:

joe:~ turbo$ ruby cat.rb /etc/resolv.cnf
cat.rb:2:in `initialize': No such file or directory - /etc/resolv.cnf (Errno::ENOENT)
  from cat.rb:2:in `open'
  from cat.rb:2:in `cat'
  from cat.rb:7:in `<main>'

Stack Trace#

The stack trace contains the information about the exception. It is quite useful to debugging: the first line indicates where (cat.rb:2 means in the file cat.rb at the 2nd line) and what (No such file or directory) happened. There is the name of the exception object as well - Errno::ENOENT (so it is ENOENT constant from Errno module). Next lines show the actual stack of the method (with the file names and the line numbers), which means the exception was raised in the method ‘open’, called from the method ‘cat’, called from the method ‘<main>’ (the main program flow).

Handling the Exception#

The exceptions are possible to handle. This means in case of the exception your program can do something with this error and continue working. To handle the exception, you must show where in the program an exception may occur. It is done with begin-rescue-end statement, with syntax:

begin
  ... # lines of code - normal program flow
rescue ExceptionName
  ... # lines of code - exception handler for ExceptionName
rescue AnotherExceptionName
  ... # lines of code - exception handler for AnotherExceptionName
else
  ... # lines of code - handler for the other exceptions
ensure
  ... # lines of code - run always
end

Program executes the lines between begin and rescue. In case of exception it is looking for the exception name after any of rescue keyword and if found, it is executing the code corresponding to specified exception. When the exception name is not found, the chunk of code after else keyword is being evaluated. Finally, the code after ensure is being run, regardless the exception was raised or not.

We know now how to handle the exception, let’s try it in our small example:

def cat(file)
  begin
    f = open(file)
    puts f.read
    f.close
    puts "### end of file: #{file} ###"
  rescue Errno::ENOENT => e
    puts "Can't cat the file: #{file} because of exception: #{e.message}"
  end
end
cat ARGV[0]
puts "### end of program ###"

Now the whole method body is under supervision of the exception hander. Why not only the open method? Well, we do not want to run f.read when the file is not found or not readable.

In this version of program in case of wrong filename script generates the proper message and continues working (so ‘### end of program ###’ is printed out). Notice the => e entry after exception name, this means that the exception object will be assigned to local variable e, which can be used, as in the example, to show the details of the exception by running the message method.

joe:~ turbo$ ruby cat.rb /etc/paaaassswd
Can't open the file: /etc/paaaassswd because of exception: No such file or directory - /etc/paaaassswd
### end of program ###

In this case we handle the only one exception - ENOENT. All other exceptions still breaks the program flow and generates stack trace:

joe:~ turbo$ ruby cat.rb /etc/ssh_host_key
cat.rb:3:in `initialize': Permission denied - /etc/ssh_host_key (Errno::EACCES)
  from cat.rb:3:in `open'
  from cat.rb:3:in `cat'
  from cat.rb:11:in `&lt;main>'

It is easy add the handler to Errno::EACCES exception to the existing one:

def cat(file)
  begin
    f = open(file)
    puts f.read
    f.close
    puts "### end of file: #{file} ###"
  rescue Errno::ENOENT, Errno::EACCES => e
    puts "Can't open the file: #{ARGV[0]} because of exception: #{e.message}"
  end
end
cat ARGV[0]
puts "### end of program ###"

Now the program behaves as expected.

joe:~ turbo$ ruby cat.rb /etc/ssh_host_key
Can't open the file: /etc/ssh_host_key because of exception: Permission denied - /etc/ssh_host_key
### end of program ###

Handling Every Exception#

It is possible to handle every exception, but it is not a good practice, because when something really unpredictable occurs, debugging starts to be a nightmare. For example, pressing Ctrl + C while executing Ruby code raises an exception as well.

General rule is to handle known exceptions and leave the rest. It is better for the script to crash in case of unhandled error!

Raising the Exception#

You can throw any exception by calling raise [ExceptionName], message statement. The first argument is optional, if you ommit it, RuntimeException will be thrown. The second argument is just a string message with the exception details.
Because the exception is - like everything - an object, to define your own one you must create the class inheriting from Exception class. Let’s go back to our Ip class:

class IpAddressException < Exception; end
class Ip
  def initialize(*parts)  # with this syntax you can provide multiple arguments, 'parts' becames an array
    if parts[0].is_a? String
      parts = parts[0].split(".").map { |x| x.to_i }
    end
    raise IpAddressException, "IP address should be 4 bytes long, given: #{parts.count}" unless parts.count == 4
    parts.each { |part| raise IpAddressException, "Not a byte: #{part}" unless (0..255) === part }
    @parts = parts
  end
  def to_s
    @parts.join('.')      # 'join' converts an array to the string with given string between
  end
  def inspect
    "IP: #{to_s}"
  end
end

At the beginning lets take a look on the new way to passing the argument to the function - def function(*arguments). With this syntax you may pass any numer of arguments, separated by the colon, and all of them will be combined to an array. If you do not pass any args, the array becomes empty.

But giving IP adress as an array of bytes is not very convinient. Sometimes it would be better to just pass the string with standar ip-address dot format. So, in lines 4-6 program checks if the first argument is a string, and if yes, converts it to the array of strings by dividing it to the substrings based on the delimiter of dot using split method. Because the result of split is an array of strings, the next step is to convert it to integers, applying to_i to all member of the array. At the end we have an array of integer parts of the IP address.

Notice that there is an opposite method to split - join, which gets the array and the delimiter and converts the array to the delimiter separated string, for example [1,2,3].join(’, ‘) #=> “1, 2, 3”.

Let’s go back to the exception. In the first line new exception IpAddressException is defined. It is a subclass of the Exception class and that is enough - no need to overwrite any methods. We want to check if the IP address given to the constructor is valid. First, in line 7, program checks if there are exactly 4 parts of the IP address. If not, it raises IpAddressException with a proper message. Next it iterates through each part of the address and raises IpAddressException if any of the parts is not a byte.

ip = Ip.new(192, 168, 1, 3)    # proper IP address
#=> IP: 192.168.1.3

ip = Ip.new('192.168.1.3')     # proper IP address, parsed from the string
#=> IP: 192.168.1.3

ip = Ip.new(192, 168, 1)       # too short!
IpAddressException: IP address should be 4 bytes long, given: 3
  from (irb):56:in `initialize'
  ...

ip = Ip.new('192.168.1.300')   # not an IP
IpAddressException: Not a byte: 300
  from (irb):57:in `block in initialize'
  ...