Learn more on ruby with our tutorial Setting Up Automated Testing with RSpec on SitePoint.
Imagine you’re riding a bike. Now, imagine the designers of that bike built it so it rides smoothly only on roads without bumps and encountering one would result in the entire bicycle breaking! You wouldn’t want that, would you? Yet this is how thousands of software developers design their software every single day. They put error handling in as an afterthought, dealing with it only when it’s inevitable.
The truth is, it’s not their fault. Most of the material on this subject is very basic, covering simple things like raising an error, rescuing it, different error types and…that’s about it. This article will attempt to go deeper than that. I assume you’re familiar with the basics of error handling (using raise, begin/rescue, what StandardError is, error inheritance). That’s the only prerequisite for reading this article. Let’s begin.
What Did We Do Before Raising/Handling Exceptions?
Before exceptions were invented, the primary method of communication that something in the program has failed was through error return codes. As time passed, people looked at ways to clearly distinguish between what their program does and what would happen if it didn’t do what it was supposed to (return codes were far from ideal for this purpose) do. Thus, the invention of language constructs like:
raise
rescue
begin/end
(Many other languages use different wording, liketry/catch
orthrow
, but the idea behind it remains the same.)
There are opposing views to using exceptions and error handling in the first place. Some of these points make sense and we’ll discuss them later in the article. For now, let’s get you familiar with some of the ways of handling errors in Ruby that can help you manage them better.
Cleanup Before Crashing
Often we have no idea when our program is going to crash. What if we needed to do some cleanup operations before our program ends (due to an error)? That’s where at_exit comes to the rescue:
puts 'Hello'
at_exit do
puts 'Exiting'
end
raise 'error'
This program will print both “Hello” and “Exiting”. The code inside at_exit
will execute when the program exits (whether it’s normally or with an exception). If you want it to run only when an exception is raised, use the global variable $!
in Ruby. The default value of $!
is nil
, it is set to reference the exception object when an error occurs:
puts 'Hello'
at_exit do
if $! # If the program exits due to an exception
puts 'Exiting'
end
end
raise 'error' # Try running this code with this line and then remove it. See when "Exiting" will print.
People use at_exit
for all sorts of things, like making your own error crash logger (printing the message in $!
to a file), communicating to another app that the running application is no longer running and so on.
You can also pass a Proc object to at_exit
instead of a block, just like with any other Ruby method that accept blocks:
puts 'Hello'
to_execute = Proc.new { puts 'Exiting' }
at_exit(&to_execute) # & converts the Proc object to a block
Exit With a Bang
Let’s assume you’ve implemented at_exit
in your program. There are instances, however, when you don’t want the block inside it to execute. What should you do in that case? Should you delete/comment out everything inside at_exit
and run your program again?
There’s a better way to add and remove code all the time. In Ruby, there’s a method called exit!
which is a “dangerous version” of exit
. The major “danger” is that when you call it, there are two things happening that wouldn’t ordinarily happen if using the regular version:
- The program will quit without executing the code block inside
at_exit
. - The exit code is being set to 1 instead of 0 as it is with the regular (non-bang) version.
Simple Error Logging Anywhere In Your Code
I’ll share my favorite method of implementing your own exception logger (If you’re interested in more ways of doing this, I highly recommend Exceptional Ruby by Avdi Grimm, the most comprehensive resource on this subject.) This approach can be used for all sorts of purposes once you grasp the main idea.
We can use a thread-local variable in Ruby to store our error messages:
(Thread.current[:errors] ||= []) << 'Any error message goes here'
Thread.current[:errors]
as a variable is available anywhere in your code (I’m normally against variables with global scope, with rare exceptions like this one. Think of this variable as a very simple global error handler.) Technically, thread-local variables have a thread scope, which should be meaningless if you’re not writing multi-threaded code. If you do, know that the name of the variable says it all; it is local to a thread (thread = your program if you’re not working with multiple threads).
You can use the above expression anywhere in your code, from any method.
To avoid repeating code, I will wrap the Thread.current
expression into a separate method (be sure the method is available from anywhere in your code):
def report_error(error_message)
(Thread.current[:errors] ||= []) << "#{error_message}"
end
Let’s say we have some_random_method
which crashes and we want to log the details:
def some_random_method
begin
raise 'an error!'
rescue => error
report_error("#{error.class} and #{error.message}")
end
end
some_random_method
p Thread.current[:errors] #=> ["RuntimeError and an error!"]
For now, the errors are being stored in thread-local variables. We want to store them in a file. Let’s write log_errors
method that will do this:
def log_errors
File.open('errors.txt', 'a') do |file|
(Thread.current[:errors] ||= []).each do |error|
file.puts error
end
end
end
This is not enough, though. We want this method to execute once the program exits, and it doesn’t matter if it exits with or without an error. As we’ve seen earlier, this is pretty easy to do: just put the method call in at_exit
:
at_exit { log_errors }
Be Careful With Your Rescue Code
One thing I have learned the hard way is that you can never be too careful with the code you put into rescue
. What if the code inside it fails and produces an exception? Sure, you can nest rescue
s, but that defeats the whole purpose of the statement!
Imagine if your program was responsible for, say, constructing a building. Then a fire occurs. As a response to it, a fireman comes to the rescue:
begin
# building the building
rescue Fire
Fireman.dispatch
end
We want to make sure the fireman does his job perfectly. We don’t want for him to catch fire by having faulty equipment! Also, we don’t want for the fireman’s vehicle to break down on the way to the building. We want everything to be flawless, and that includes 0% failure rate. That should be the ultimate goal with your code inside the rescue
statement. As I’ve mentioned, you can go deeper and get someone to rescue the rescuer, which could lead to problems.
Never Rescue Exception
, Never Rescue Broadly
After reading 2 or 3 articles on the basics of Ruby exception handling, you’re bound to see the advice on never rescuing Exception
. Exception
is the root of the exception class library, the “mother of all exceptions.” I want to go even further with this advice and recommend you never rescue broadly. That includes ignoring broad classes like StandardError
(StandardError
itself has over 300 child classes descending from it). So basically, by rescuing StandardError
, you’re handling 300 potential failure cases. Now tell me, if you have a rescue block handling 300 possible failure cases, what’s the probability of the rescue
block failing itself? Imagine giving a fireman the same equipment to deal with single floor houses and an 100-story building! Not a good idea.
So, what’s the solution? Instead of going broad, try to rescue specific errors (which don’t have 100+ children exceptions). Which leads me to my next point…
Types of Exceptions (By Probability of Happening)
Many people preach things like “exceptions should be exceptional” without realizing the word “exceptional” is frequently misunderstood. I propose a better way of categorizing a particular exception, by the probability of it actually happening:
- Possible to happen. This is what many people mean when they say an exception should be “exceptional”. An exception that would happen under a situation that is far from what you can expect.
- Likely to happen. You can predict with fair accuracy that under some situation, this exception will eventually happen.
- Must (is required to) happen The exception will happen, and you can create the situation for that quite easily.
Let’s tackle the third (last) type first. If your exception is required to happen or has a very high probability of happening, re-think if you even need to raise it in the first place. If you’re raising, are you using this exception for control flow? This is a bad idea. For example, raising an exception if a user entered incorrect input instead of prompting again (using some type of loop). If you’re rescuing an exception that happens a lot and is raised under some library, see if you can wrap the whole logic into some sort of validation method where you’ll get a return value instead of constantly crashing.
If the exception is possible to happen but not likely, do you really need to handle it? Do you have to foresee and distinguish every possible case? This is a main reason why so many people rescue StandardError
or Exception
. They don’t want their program to fail under any circumstance. In my experience (and that of many other people I’ve talked to) this creates more problems than it solves. My recommendation here is to simply run your program often enough and see where it fails. See the type of exceptions raised and when they happen, say, more than twice, deal with it.
Likely to happen. This is the sweet spot when rescuing happens. You know the type of exception, you know how often/when it occurs by running your program often enough. You have an idea how often it occurs and when, so you can handle it safely.
My advice is to not rescue exceptions for which you have no idea of whether or not they will happen. If they happened once or twice and you’ve been running your program for months, examine the underlying reason for it. Ask “why” until you eventually get to an answer that would allow you to fix your program/system logic and prevent that error from ever happening again.
I wouldn’t give the same advice when you raise exceptions though. Before you do it, take a look at a few alternatives.
Alternatives to Raising Exceptions
Writing a simple raise is easy. The consequences are far from desirable, though. Your program will stop. Are you sure you want to do that? There are a few alternatives you can implement to make your program continue normally.
A Custom Strategy
You can instruct Ruby to use a custom strategy for rescuing an exception (the default would be raise). Suppose you have this code (in case you’re confused about where the begin
clause is, each method definition is an implicit begin/end
statement, meaning the def
itself is the begin
):
def some_method(some_argument, error_strategy = method(:raise))
raise "#{some_argument}"
rescue
error_strategy.call
end
some_method(1) # '1' (RuntimeError)
This code will raise a RuntimeError
with the message as our argument, which is 1
. Now, keeping the method definition intact, try the following:
error_handler = Proc.new { puts 'I rescued it!' }
some_method(1, error_handler)
Now, the program will end with no error and “I rescued it!” will print to the console. We just changed our default “strategy” for handling errors in the method by passing a proc to the object. If the method(:raise)
part is unfamiliar to you, see this article.
Use a Value That Will Allow the Program to Continue
Let’s say you have a method that’s supposed to return an array. While calling the method, you have an exception that pops up from time to time. You want to continue running though, instead of crashing your program all the time. Why not return an empty array? If the result of your method is then used to iterate over something, an empty array will make the subsequent code not iterate over anything. A sample code would make this easier to understand:
def method_that_should_return_an_array
this_method_call_will_raise_an_error('some argument')
rescue
[] # your method will return this
end
Raise nil
I think returning nil
instead of raising an exception is an often overused strategy in the Ruby community. Raise your hand if you got that “cannot call X method on NilClass” error and was frustrated to find which part of your code returned an unexpected nil
that caused all hell to break loose. To combat this problem, many people would do something like this:
some_method_that_might_return_nil || raise 'nil returned'
This code snippet is a short way to raise an exception if some expression in the same line returns nil
.
When you return nil
, it’s a good idea to make sure the code later will check and handle it (else you’ll get a bunch of unexpected “cannot call X method on NilClass” errors):
my_value = some_method_that_might_raise_nil
if my_value.nil?
# do something
else
# do something else
end
Take the advice in this article as a set of principles, not rules on what to do. Try it and see if it works for your particular circumstances. My purpose here was to expand your way of thinking about exception handling and give you new ideas and tools you can go and implement right now in your program. I hope my mission was accomplished.
Learn more on ruby with our tutorial Setting Up Automated Testing with RSpec on SitePoint.