How to Rescue Exceptions in Ruby

By: Rich
  |  September 27, 2023
How to Rescue Exceptions in Ruby

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.

What is an exception?

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.

When to handle an exception

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

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

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

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.

When exceptions occur

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.

How to handle an exception

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.

Be specific about what you rescue

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.

Multiple rescues

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.

Assigning the error to a variable

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

Don’t rescue all exceptions

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

Statement rescue

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

Rescuing loops

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.

Using retry

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]

Using next

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]

Rescue each

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.

Conclusion

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.

Improve Your Code with Retrace APM

Stackify's APM tools are used by thousands of .NET, Java, PHP, Node.js, Python, & Ruby developers all over the world.
Explore Retrace's product features to learn more.

Learn More

Want to contribute to the Stackify blog?

If you would like to be a guest contributor to the Stackify blog please reach out to [email protected]