In Ruby, we handle errors with the begin
-keyword, rescue
-keyword and ensure
-keyword. We use a raise
-statement to cause trouble.
With a begin
-statement, we enter protected blocks. We can test statements by placing them in protected blocks, discovering what exception they are causing, and where it occurs.
An exception is raised when you divide by zero. That is why you should never divide by zero—also it makes no sense. But sometimes these conditions are hard to avoid.
ZeroDivisionError
is encountered.# Enter a protected region. begin # Try to divide by zero. i = 1 / 0 rescue ZeroDivisionError # Handle the error. puts "ERROR" i = 0 end # Display final value. puts iERROR 0
Many constructs in Ruby allow an optional else
-statement. Begin and rescue also allow "else." Code in the else
-block is executed only if no errors occur within the begin
-block.
divisor = 2 begin reciprocal = 1 / divisor rescue reciprocal = 0 else # This is reached when no error is encountered in "begin." puts "NO ERROR" end puts reciprocalNO ERROR 0
Here we use the raise
-statement to raise an exception. We specify a string
argument, so the exception we create is of type RuntimeError
. And we specify this in the rescue
-block.
begin # This is a bad program. raise "Bad program error" rescue RuntimeError => e # This prints the error message. puts e endBad program error
In exception handling, we often want to allow an exception to pass through a rescue
-block. This is called "reraising" the exception.
raise
-statement, with no argument, the present exception is raised again.IndexError
. And then, in the rescue
-block, we reraise it. We then rescue it at the method's calling location.def problem(n) begin raise IndexError, n if n < 0 rescue # Reraise this error. raise end end # Call the problem method. begin problem(-1) rescue IndexError # Handle the re-raised error. puts "IndexError encountered!" endIndexError encountered!
Catch and throw provide an alternative flow, one controlled by labels. We prefix these labels with a colon, as by ":label." In a catch block, we place statements.
def method(a) puts a # Throw on a negative number. if a < 0 throw :negative end end # These statements continue until :negative is thrown. catch :negative do method(0) method(-1) puts "NOT REACHED" end puts "END"0 -1 END
The retry-statement is placed in a rescue block. When retry is reached, the begin statement is entered again. This acts like a "go to" operation.
denominator = 0 begin # Divide with the denominator integer. result = 1 / denominator puts(result) rescue # Change denominator to 1 and try again. puts("Rescuing") denominator = 1 retry endRescuing 1
Statements in an ensure
-block are always executed. We add an ensure
-clause at the end of an exception-handling block. The ensure is run regardless of whether an error is raised.
ensure
-block can be used to perform some cleanup (like deleting a temporary file). It can display a completion message.y = 10 begin x = 100 / y puts "In begin..." rescue # This is not reached. puts "In rescue..." ensure # Do some cleanup. puts "In ensure..." endIn begin... In ensure...
There is a cost to raising an exception. And usually it is faster to try to prevent exceptions from occurring. Here I tried to time an exception: the ZeroDivisionError
.
if
-statement. Raising the exception is slower.n1 = Time.now.usec # Version 1: use begin, rescue. 50000.times do 5.times do |x| begin i = 1 / x rescue i = 0 end end end n2 = Time.now.usec # Version 2: use if, else. 50000.times do 5.times do |x| if x == 0 i = 0 else i = 1 / x end end end n3 = Time.now.usec # Compute milliseconds total. puts ((n2 - n1) / 1000) puts ((n3 - n2) / 1000)171 ms begin/rescue 15 ms if/else
Exception handling is critical in Ruby. It is often the difference between a useless program that cannot be deployed, and one that is effective—or at least limps along.