Exceptions are a commonly used feature in the Ruby programming language. The Ruby standard library defines about 30 different subclasses of exceptions, some of which have their own subclasses. The exception mechanism in Ruby is very powerful but often misused. This article will discuss the use of exceptions and show some examples of how to deal with them.
An exception represents an error condition in a program. Exceptions provide a mechanism for stopping the execution of a program. They function similarly to “break,” in that they cause the instruction pointer to jump to another location. Unlike break, the location may be another layer of the program stack. Unhandled exceptions cause Ruby programs to stop.
A simple rule for handling exceptions is to handle only those exceptions you can do something about. That’s easy to say, but sometimes difficult to get right. We have a tendency to want to rescue every exception that could possibly occur. Because we often don’t know what to do about an exception, we tend to just log a message and continue execution. Often this leads to writing extra code to deal with the failures—code we don’t actually need.
We should handle exceptions only when we can reasonably take some action to correct the error and allow our program to continue functioning.
We need to think about three basic categories of exceptional behavior when writing code: possible, probable, and inevitable.
Possible exceptions are theoretically possible, but unlikely to occur in the system. When these kinds of exceptions occur, it’s typically because the system is truly broken. In this case, the situation is irrecoverable, and we shouldn’t try to handle the exceptions.
Probable exceptions may reasonably happen during our program’s execution—for example, a REST call failure caused by a DNS issue. These are the issues we can foresee when developing our program, and in some cases, we can see a resolution to these problems. This is where we should focus the bulk of our exception handling attention.
Inevitable exceptions will happen quite often. A common tendency is to allow these exceptions. A more effective approach is to proactively detect the exceptional condition and branch around it. Exceptions shouldn’t be used for flow control. Wherever possible, if we can predetermine that an operation would create an exceptional condition, we shouldn’t execute the operation.
Take a corrective action whenever an exception occurs. That is, exception handling is about civilizing the behavior of our programs. You should not bury the exceptions—begin, rescue, bury are anti-patterns that should be avoided.
That said, sometimes the only thing we can do is report that an exception has occurred. The simplest form of this reporting is to print information to standard error indicating that an exception has occurred, and possibly provide some guidance on how to correct the issue.
More sophisticated systems report issues through logging and APM tools like Retrace. APM tools simplify the process by centralizing the reporting of issues and monitoring as well as quickly identifying ways to improve performance and fix hidden exceptions.
Retrace is starting to support Ruby applications to ensure no errors slip through the cracks as deployments get pushed into production.
Ruby’s exception handling mechanism is simple: it places the keyword “rescue” after any code that would probably throw an exception. Ruby does require some form of “begin” to appear before the rescue. The general syntax for the rescue statement is as follows:
begin
#... process, may raise an exception
rescue =>
#... error handler
else
#... executes when no error
ensure
#... always executed
end
The code between “begin” and “rescue” is where a probable exception might occur. If an exception occurs, the rescue block will execute. You should try to be specific about what exception you’re rescuing because it’s considered a bad practice to capture all exceptions.
You should specify what your rescue statement can handle. If your rescue block can handle multiple erroneous conditions, use the most general class name possible. In some cases, this is StandardError, but often it’s a subtree of the class hierarchy under StandardError.
A bare rescue will capture StandardError and all of its subtypes—that is, it’ll catch any class raised that extends StandardError. This is problematic. You should rescue only those exceptions you can actually do something about. Other exceptions should be allowed to flow past your rescue statement.
If you’re using an APM tool like Retrace, it will allow you to ignore certain exceptions that you can’t fix and are just creating unwanted noise.
In cases where multiple rescues are possible and the rescue operation is different for each, you can specify multiple rescue blocks. An example might be something like this:
3.0.0 :001 > values = []
3.0.0 :002 > begin
3.0.0 :003?> File.readlines('input.txt').each { |line| values <> Float(line) }
3.0.0 :004?> rescue Errno::ENOENT
3.0.0 :005?> p 'file not found'
3.0.0 :006?> rescue ArgumentError
3.0.0 :007?> p 'file contains unparsable numbers'
3.0.0 :008?> else
2.5.3 :009?> print values
3.0.0 :010?> end
[3.0, 4.5, 9.9, 10.0] => nil
In this example, two possible issues may occur. First, the file might not be found. Second, an ArgumentError might occur if the content of input.txt can’t be parsed to a floating-point number. In each case, the rescue operation is different.
As mentioned before, each rescued exception can be assigned to a variable. This allows you to inspect the error that occurred. In the following example, all StandardErrors are captured by the rescue, and the exception message is printed:
3.0.0 :013 > begin
3.0.0 :014?> IO.readlines('input.txt').each { |line| values << Float(line) } 3.0.0 :015?> rescue => error
3.0.0 :016?> p error.message
3.0.0 :017?> end
"invalid value for Float(): "fooie\n""
You can force Ruby to capture all possible exceptions (except fatal exceptions, which are not rescuable) by specifying the class name “exception” in the rescue. However, you never want to do this. Exceptions outside of the StandardError hierarchy are used in the general function of the Ruby environment. By catching them, you can break your program in weird and unexpected ways. Consider the following example:
3.0.0 :001 > points_scored = 100.0
3.0.0 :002 > points_possible = nil
3.0.0 :003 > begin
3.0.0 :004?> grade = points_scored / points_possible
3.0.0 :005?> rescue TypeError
3.0.0 :006?> p "The instructor did not provide a value for points possible"
3.0.0 :007?> grade = 0.0
3.0.0 :008?> else
3.0.0 :009?> p "Your grade is #{grade}%"
3.0.0 :010?> ensure
3.0.0 :011 > p "Grade Report Complete"
3.0.0 :012?> end
"The instructor did not provide a value for points possible"
"Grade Report Complete"
=> 0.0
The syntax of a statement rescue is as follows:
rescue ...error handler...
This can be useful for dealing with simple issues in which a potential exception may occur. For example:
3.0.0 :001 > points_scored = 100.0
3.0.0 :002 > points_possible = nil
3.0.0 :003 > grade = points_scored / points_possible rescue 0.0
=> 0.0
In this case, it’s possible for the math on line 3 to fail. By using nil here, we cause a TypeError. The rescue block will catch all StandardError exceptions and set the grade to zero. This can be a handy shortcut for dealing with these scenarios. You can even place an entire block of code in the rescue here if there are multiple steps to the correction.
3.0.0 :001 > score = 80.0
3.0.0 :002 > possible_score = nil
3.0.0 :003 > grade = score / possible_score rescue begin
3.0.0 :004?> print 'math error'
3.0.0 :005?> 0.0
3.0.0 :006?> end
math error => 0.0
3.0.0 :007 > grade
=> 0.0
The “rescue” keyword can be applied to loops as well. After all, loops are just statements in Ruby. The syntax for rescue on the various loops looks like this:
while do
#... loop body
end rescue ...error handler...
begin
#... loop body
end while rescue ...error handler...
until do
#... loop body
end rescue ...error handler...
for in expression do
#... loop body
end rescue ...error handler...
There are some things to consider when using a rescue on a loop. Specifically, the rescue is executed after the loop terminates.
For example:
3.0.0 :001 > scores = [80.0, 85.0, 90.0, 95.0, 100.0]
3.0.0 :002 > possibles = [100.0, 100.0, 100.0, nil, 100.0]
3.0.0 :003 > grades = []
3.0.0 :004 > for idx in 0..(scores.length-1)
3.0.0 :005?> grades[idx] = scores[idx] / possibles[idx]
3.0.0 :006?> end rescue grades[idx] = 0.0
3.0.0 :007 > grades
=> [0.8, 0.85, 0.9, 0.0]
Of course, this causes a problem. The last score/possibles pair wasn’t evaluated, which isn’t an ideal solution. Ruby provides a way to retry the block between “begin” and “retry” that can fix this problem.
In the following example, we’ll build out a full rescue block and then use that rescue block to correct the error condition. Then we will use “retry” to re-execute starting from the begin block. This will give us an answer for each score.
3.0.0 :001 > scores = [80.0, 85.0, 90.0, 95.0, 100.0]
3.0.0 :002 > possibles = [100.0, 100.0, 100.0, nil, 100.0]
3.0.0 :008 > for idx in 0..(scores.length-1)
3.0.0 :009?> begin
3.0.0 :010?> grades[idx] = scores[idx] / possibles[idx]
3.0.0 :011?> rescue TypeError
3.0.0 :012?> possibles[idx] = 100.0
3.0.0 :013?> retry
3.0.0 :014?> end
3.0.0 :015?> end
=> 0..4
3.0.0 :016 > grades
=> [0.8, 0.85, 0.9, 0.95, 1.0]
Although next isn’t actually part of the rescue mechanism, we can make the previous example less presumptive. A TypeError is raised when no “possibles” value exists in the previous example. We’re setting a value in possibles and retrying the math. This is fine if we understand that the correct value of the possibles is 100.0 for any given evaluation. If that’s not appropriate, we can use a sentinel value to indicate that a computation error occurred and use “next” to cause the loop to proceed to the next evaluation.
3.0.0 :001 > scores = [80.0,85.0,90.0,95.0,100.0]
3.0.0 :002 > possibles = [80.0,110.0,200.0,nil,100.0]
3.0.0 :003 > grades=[]
3.0.0 :004 > for idx in 0..(scores.length-1)
3.0.0 :005?> begin
3.0.0 :006?> grades[idx] = scores[idx] / possibles[idx]
3.0.0 :007?> rescue TypeError
3.0.0 :008?> grades[idx] = 'Computation Error'
3.0.0 :009?> next
3.0.0 :010?> end
3.0.0 :011?> end
3.0.0 :012 > grades
=> [1.0, 0.7727272727272727, 0.45, "Computation Error", 1.0]
You may be wondering if you can use “rescue” with an “each” iterator. The answer is yes. The syntax is as follows:
.each {} rescue ...error handler...
For example:
3.0.0 :001 > values = [1,2,3,0]
3.0.0 :002 > results = []
3.0.0 :003 > values.each { |value| results <<; value / value } rescue results <;< 'undefined' 2.5.3 :004 > results
=> [1, 1, 1, "undefined"]
For a deeper understanding on how to handle exceptions in Ruby refer to the official Ruby Exception Handling documentation.
The rescue block in Ruby is very powerful. It’s vastly easier to use than error codes. Rescue lets you create more robust solutions by providing a simple way to deal with common errors that might occur in your program. At a minimum, you can provide a graceful shutdown and reporting of problems in your code. If you’re looking for something more robust, check out Retrace for Ruby.
If you would like to be a guest contributor to the Stackify blog please reach out to [email protected]