Raising An Exception
An exception is a special kind of object, an instance of the class Exception or a descendant of that class that represents some kind of exceptional condition; it indicates that something has gone wrong. When this occurs, an exception is raised (or thrown). By default, Ruby programs terminate when an exception occurs. But it is possible to declare exception handlers. An exception handler is a block of code that is executed if an exception occurs during the execution of some other block of code. Raising an exception means stopping normal execution of the program and transferring the flow-of-control to the exception handling code where you either deal with the problem that's been encountered or exit the program completely. Which of these happens - dealing with it or aborting the program - depends on whether you have provided a rescue clause (rescue is a fundamental part of the Ruby language). If you haven't provided such a clause, the program terminates; if you have, control flows to the rescue clause.
Ruby has some predefined classes - Exception and its children - that help you to handle errors that can occur in your program. The following figure shows the Ruby exception hierarchy.
Reference: The above figure is from the Programming Ruby book.
The chart above shows that most of the subclasses extend a class known as StandardError. These are the "normal" exceptions that typical Ruby programs try to handle. The other exceptions represent lower-level, more serious, or less recoverable conditions, and normal Ruby programs do not typically attempt to handle them.
The following method raises an exception whenever it's called. Its second message will never be printed. Programp043raise.rb
- # p043raise.rb
- def raise_exception
- puts 'I am before the raise.'
- raise 'An error has occured'
- puts 'I am after the raise'
- end
- raise_exception
The output is:
- >ruby p043raise.rb
- I am before the raise.
- p043raise.rb:4:in `raise_exception': An error has occured (RuntimeError)
- from p043raise.rb:7
- >Exit code: 1
The raise method is from the Kernel module. By default, raise creates an exception of the RuntimeError class. To raise an exception of a specific class, you can pass in the class name as an argument to raise. Refer programp044inverse.rb
- # p044inverse.rb
- def inverse(x)
- raise ArgumentError, 'Argument is not numeric' unless x.is_a? Numeric
- 1.0 / x
- end
- puts inverse(2)
- puts inverse('not a number')
The output is:
- >ruby p044inverse.rb
- 0.5
- p044inverse.rb:3:in `inverse': Argument is not numeric (ArgumentError)
- from p044inverse.rb:7
- >Exit code: 1
Remember, methods that act as queries are often named with a trailing ?. is_a? is a method in the Object class and returns true or false. The unless modifier when tacked at the end of a normal statement means execute the preceding expression unless condition is true.
Defining new exception classes: To be even more specific about an error, you can define your own Exceptionsubclass:
- class NotInvertibleError < StandardError
- end
Handling an Exception
To do exception handling, we enclose the code that could raise an exception in a begin-end block and use one or more rescue clauses to tell Ruby the types of exceptions we want to handle. It is to be noted that the body of a method definition is an implicit begin-end block; the begin is omitted, and the entire body of the method is subject to exception handling, ending with the end of the method.
The program p045handexcp.rb illustrates this:
- # p045handexcp.rb
- def raise_and_rescue
- begin
- puts 'I am before the raise.'
- raise 'An error has occured.'
- puts 'I am after the raise.'
- rescue
- puts 'I am rescued.'
- end
- puts 'I am after the begin block.'
- end
- raise_and_rescue
The output is:
- >ruby p045handexcp.rb
- I am before the raise.
- I am rescued.
- I am after the begin block.
- >Exit code: 0
Observe that the code interrupted by the exception never gets run. Once the exception is handled, execution continues immediately after the begin block that spawned it.
If you write a rescue clause with no parameter list, the parameter defaults to StandardError. Each rescue clause can specify multiple exceptions to catch. At the end of each rescue clause you can give Ruby the name of a local variable to receive the matched exception. The parameters to the rescue clause can also be arbitrary expressions (including method calls) that return an Exception class. If we use raise with no parameters, it re-raises the exception.
You can stack rescue clauses in a begin/rescue block. Exceptions not handled by one rescue clause will trickle down to the next:
- begin
- # -
- rescue OneTypeOfException
- # -
- rescue AnotherTypeOfException
- # -
- else
- # Other exceptions
- end
For each rescue clause in the begin block, Ruby compares the raised Exception against each of the parameters in turn. The match will succeed if the exception named in the rescue clause is the same as the type of the currently thrown exception, or is a superclass of that exception. The code in an else clause is executed if the code in the body of the begin statement runs to completion without exceptions. If an exception occurs, then the else clause will obviously not be executed. The use of an else clause is not particularly common in Ruby.
If you want to interrogate a rescued exception, you can map the Exception object to a variable within the rescueclause, as shown in the program p046excpvar.rb
- # p046excpvar.rb
- begin
- raise 'A test exception.'
- rescue Exception => e
- puts e.message
- puts e.backtrace.inspect
- end
The output is:
- >ruby p046excpvar.rb
- A test exception.
- ["p046excpvar.rb:3"]
- >Exit code: 0
The Exception class defines two methods that return details about the exception. The message method returns a string that may provide human-readable details about what went wrong. The other important method is backtrace. This method returns an array of strings that represent the call stack at the point that the exception was raised.
If you need the guarantee that some processing is done at the end of a block of code, regardless of whether an exception was raised then the ensure clause can be used. ensure goes after the last rescue clause and contains a chunk of code that will always be executed as the block terminates. The ensure block will always run.
Some common exceptions are:
RuntimeError (this is the default exception raised by the raise method), NoMethodError, NameError, IOError,TypeError and ArgumentError.
RuntimeError (this is the default exception raised by the raise method), NoMethodError, NameError, IOError,TypeError and ArgumentError.
An Example: Let's modify program p027readwrite.rb to include exception handling as shown in examplep046xreadwrite.rb below.
- # p046xreadwrite.rb
- # Open and read from a text file
- # Note that since a block is given, file will automatically be closed when the block terminates
- begin
- File.open('p014constructs.rb', 'r') do |f1|
- while line = f1.gets
- puts line
- end
- end
- # Create a new file and write to it
- File.open('test.rb', 'w') do |f2|
- # use "" for two lines of text
- f2.puts "Created by Satish\nThank God!"
- end
- rescue Exception => msg
- # display the system generated error message
- puts msg
- end
Improper error messages can provide critical information about an application which may aid an attacker in exploiting the application. The most common problem occurs when detailed internal error messages such as stack traces, database dumps, and error codes are displayed to the user. Security analysts view logging and error handling as potential areas of risk. It is recommended that production applications should not use, for example, a puts e.backtrace.inspect call unless it is being directly committed into a log that is not viewable to the end user.
Validation example
Here's an example from the Ruby Cookbook, showing how one can do validation of user's inputs.
- class Name
- # Define default getter methods, but not setter methods.
- attr_reader :first, :last
- # When someone tries to set a first name, enforce rules about it.
- def first=(first)
- if first == nil or first.size == 0
- raise ArgumentError.new('Everyone must have a first name.')
- end
- first = first.dup
- first[0] = first[0].chr.capitalize
- @first = first
- end
- # When someone tries to set a last name, enforce rules about it.
- def last=(last)
- if last == nil or last.size == 0
- raise ArgumentError.new('Everyone must have a last name.')
- end
- @last = last
- end
- def full_name
- "#{@first} #{@last}"
- end
- # Delegate to the setter methods instead of setting the instance
- # variables directly.
- def initialize(first, last)
- self.first = first
- self.last = last
- end
- end
- jacob = Name.new('Jacob', 'Berendes')
- jacob.first = 'Mary Sue'
- jacob.full_name # => "Mary Sue Berendes"
- john = Name.new('john', 'von Neumann')
- john.full_name # => "John von Neumann"
- john.first = 'john'
- john.first # => "John"
- john.first = nil
- # ArgumentError: Everyone must have a first name.
- Name.new('Kero, international football star and performance artist', nil)
- # ArgumentError: Everyone must have a last name.
The Name class keeps track of peoples' first and last names. It uses setter methods to enforce two somewhat parochial rules: everyone must have both a first and a last name, and everyone's first name must begin with a capital letter. The Name class has been written in such a way, that the rules are enforced both in the constructor and after the object has been created. Sometimes you don't trust the data coming in through the setter methods. That's when you can define your own methods to stop bad data before it infects your objects. Within a class, you have direct access to the instance variables. You can simply assign to an instance variable and the setter method won't be triggered. If you do want to trigger the setter method, you'll have to call it explicitly. Note how, in the Name#initialize method above, we call the first= and last= methods instead of assigning to @first and @last. This makes sure the validation code gets run for the initial values of every Name object. We can't just say first = first, because first is a variable name in that method.