Ruby Exception Examples: Begin and Rescue

This Ruby page focuses on exceptions. It uses the begin, rescue and ensure keywords to enter protected regions.

Exceptions. Bugs crawl about. Programs often cause errors.

A file may not exist. A user may input invalid data. We protect against these errors with exceptions.

 

 

Keywords. In Ruby, we wield the begin, rescue and ensure keywords. We use raise to cause trouble. With begin, we enter protected blocks.

 

 

An example. 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.

 

Note: A Ruby program would normally terminate when the ZeroDivisionError is encountered.

However: If you enclose the division statement in a begin block, we have an opportunity to catch or "rescue" the error.

Rescue: In this program we rescue an invalid division expression. We set the result to zero.

Based on:

Ruby 2

Ruby program that uses begin, rescue

# 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 i

Output

ERROR
0

Else. 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.

Note: Usually a message that states no errors occurred is not needed, but it works well for demonstration.

Tip: In real programs, exceptions usually should not be the "normal" case. Normal operation should have no exceptional events.

Ruby program that uses else

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 reciprocal

Output

NO ERROR
0

Raise. 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.

Type: Raise can create an exception of any type. We can specify the type as the first argument to "raise."

Ruby program that uses raise

begin
    # This is a bad program.
    raise "Bad program error"
rescue RuntimeError => e
    # This prints the error message.
    puts e
end

Output

Bad program error

Reraise. In exception handling, we often want to allow an exception to pass through a "rescue" block. This is called "reraising" the exception.

And: With a raise statement, with no argument, the present exception is raised again.

However: We trigger an IndexError. And then, in the "rescue" block, we reraise it. We then rescue it at the method's calling location.

Ruby program that reraises an error

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!"
end

Output

IndexError encountered!

Catch, throw. 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.

Throw: If a throw occurs within the statements in a catch block, the catch block is exited.

Tip: With throw and catch, the labels are matched. In many ways these statements act like "goto" but can pass method boundaries.

Also: For nested loops and method calls, catch and throw can be useful. With them we can reduce the need for flag variables to direct flow.

Ruby program that uses catch, throw

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"

Output

0
-1
END

Retry. 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.

Note: We usually change variables or an external file before retrying. We attempt to correct the program's state.

Ruby program that uses retry

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
end

Output

Rescuing
1

Ensure. 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.

Tip: An ensure block can be used to perform some cleanup (like deleting a temporary file). It can display a completion message.

Ruby that uses ensure

y = 10

begin
    x = 100 / y
    puts "In begin..."
rescue
    # This is not reached.
    puts "In rescue..."
ensure
    # Do some cleanup.
    puts "In ensure..."
end

Output

In begin...
In ensure...

Performance. 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.

Version 1: This code runs a loop 50,000 times. Every 5 iterations, an exception is raised. We handle it with rescue.

Version 2: This code checks the iterator variable "x" against zero each time. It handles zero with a special case.

Iterator, Times

Result: It is faster, for a frequent error, to test values with an if-statement. Raising the exception is slower.

Ruby that times begin, if

n1 = Time.now.usec

# Version 1: 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: 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)

Output

171 ms    begin/rescue
 15 ms    if/else

Exceptions are useful. Placing statements in begin and rescue blocks is a good way to test them. You can accurately discover what exception they are causing, and where it occurs.

Tip: If a non-essential part of code fails, you may be able to continue the rest of the program without much concern.

Tip 2: Logging an error code in your application is a good way to later correct or debug the application.

 

Exception handling is critical. It is often the difference between a useless program that cannot be deployed, and one that is effective (or at least limps along). Programs must be robust.