Getting Started with Ruby Exceptions
This article is about exceptions in Ruby. It discusses the hierarchy of exception classes, how to handle exceptions in your code, raising exceptions manually, and using custom exception classes.
Note: This article was originally published on the Launch School blog on 2017–08–14
In this article we will introduce the basics of working with exceptions in Ruby. It is likely that you have already encountered exceptions in your Ruby programs, but you may not have a complete understanding of where these errors come from. To begin, we will discuss what an exception is, as well as various types of exceptions and their severity. We will then introduce several basic techniques for handling common exceptions when they occur in your code. Lastly, we will explore raising your own exceptions and using custom exception classes.
What is an Exception?
An exception is simply an exceptional state in your code. It is not necessarily a bad thing, but it is Ruby’s way of letting you know that your code is behaving unexpectedly. If an exception is raised and your code does not handle the exception, your program will crash and Ruby will provide a message telling you what type of error was encountered.
3 + "z"
# Program execution stops
#=> String can't be coerced into Integer (TypeError)
Ruby provides a hierarchy of built-in classes to simplify exception handling. In fact, the exception names that you see when your program crashes, such as TypeError
, are actually class names. The class at the very top of the hierarchy is the Exception
class. Exception
has several subclasses, many of which have descendents of their own.
The Exception Class Hierarchy
Below you can see the complete hierarchy of Ruby’s exception classes.
Exception
NoMemoryError
ScriptError
LoadError
NotImplementedError
SyntaxError
SecurityError
SignalException
Interrupt
StandardError
ArgumentError
UncaughtThrowError
EncodingError
FiberError
IOError
EOFError
IndexError
KeyError
StopIteration
LocalJumpError
NameError
NoMethodError
RangeError
FloatDomainError
RegexpError
RuntimeError
SystemCallError
Errno::*
ThreadError
TypeError
ZeroDivisionError
SystemExit
SystemStackError
fatal
Let’s take a moment to briefly examine some of the classes in this hierarchy and think about when you might encounter them. If you have been writing Ruby code for any length of time, it’s likely that you have already seen some of these exceptions raised in your own programs.
- Have you ever pressed
ctrl-c
to exit out of a program? Doing so actually raises an exception via theInterrupt
class. - A
SyntaxError
, as its name suggests, will be raised when Ruby tries to execute code containing invalid syntax. This probably looks familiar if you have ever mistakenly left adef
orend
off of a method definition. - A
SystemStackError
is raised in the case of a stack overflow. You may have seen this exception if you have run a recursive infinite loop in your program. StandardError
has many recognizable descendents.ArgumentError
,TypeError
,ZeroDivisionError
, andNoMethodError
are all common exceptions that are children or grandchildren of theStandardError
class.
When Should You Handle an Exception?
Most often, the errors you want to handle are descendents of the StandardError
class that was introduced above. These exceptions may be caused by a wide variety of circumstances including unexpected user input, faulty type conversions, or dividing by zero. Generally, it is relatively safe to handle these exceptions and continue running the program.
Why not just handle all exceptions? Doing so can be very dangerous. Some exceptions are more serious than others; there are some errors that we should allow to crash our program. Important errors such as NoMemoryError
, SyntaxError
, and LoadError
must be addressed in order for our program to operate appropriately. Handling all exceptions may result in masking critical errors and can make debugging a very difficult task.
In order to avoid causing unwanted behaviors yourself, it is important to be intentional and very specific about which exceptions you want to handle and what action you want to take when you handle them. The action you choose to take when handling an exception will be dependent on the circumstances; examples include logging the error, sending an e-mail to an administrator, or displaying a message to the user.
How to Handle an Exceptional State
The begin
/rescue
Block
Using a begin
/rescue
block to handle errors can keep your program from crashing if the exception you have specified is raised. Let's see a simple example.
begin
# put code at risk of failing here
rescue TypeError
# take action
end
The above example will execute the code in the rescue
clause rather than exiting the program if the code on line 2 raises a TypeError
. If no exception is raised, the rescue
clause will not be executed at all and the program will continue to run normally. You can see that on line 3 we specified what type of exception to rescue. If no exception type is specified, all StandardError
exceptions will be rescued and handled. Remember not to tell Ruby to rescue Exception
class exceptions. Doing so will rescue all exceptions down the Exception
class hierarchy and is very dangerous, as explained previously.
It is possible to include multiple rescue
clauses to handle different types of exceptions that may occur.
begin
# some code at risk of failing
rescue TypeError
# take action
rescue ArgumentError
# take a different action
end
Alternatively, if you would like to take the same action for more than one type of exception, you can use the syntax on line 3 below.
begin
# some code at risk of failing
rescue ZeroDivisionError, TypeError
# take action
end
Exception Objects and Built-In Methods
Exception objects are just normal Ruby objects that we can gain useful information from. Ruby provides built-in behaviors for these objects that you may want to use while handling the exception or debugging. Take a look at Ruby’s Exception
documentation.
So how do we use an exception object?
rescue TypeError => e
The syntax in the above code rescues any TypeError
, and stores the exception object in e
. Some useful instance methods that Ruby provides are Exception#message
and Exception#backtrace
, which return an error message and a backtrace associated with the exception, respectively. Let's look at an example of this in a begin
/rescue
block.
begin
# code at risk of failing here
rescue StandardError => e # storing the exception object in e
puts e.message # output error message
end
The code above will rescue any type of StandardError
exception (including all of its descendents) and output the message associated with the exception object. Code like this can be useful when you are debugging and need to narrow down the type or cause of the error. You may always choose to be more specific about which type of exception to handle, but remember to never rescue the Exception
class.
Recall that exception objects are just normal Ruby objects and the different exception types, such as ArgumentError
and NoMethodError
, are actually class names. Therefore, we can even call Object#class
on an exception object to return its class name.
e.class
#=> TypeError
ensure
You may also choose to include an ensure
clause in your begin
/rescue
block after the last rescue
clause. This branch will always execute, whether an exception was raised or not. So, when is this useful? A simple example is resource management; the code below demonstrates working with a file. Whether or not an exception was raised when working with the file, this code ensures that it will always be closed.
file = open(file_name, 'w')begin
# do something with file
rescue
# handle exception
rescue
# handle a different exception
ensure
file.close
# executes every time
end
If there are multiple rescue
clauses in the begin
/rescue
block, the ensure
clause serves as a single exit point for the block and allows you to put all of your cleanup code in one place, as seen in the code above.
One important thing to remember about ensure
is that it is critical that this code does not raise an exception itself. If the code within the ensure
clause raises an exception, any exception raised earlier in the execution of the begin
/rescue
block will be masked and debugging can become very difficult.
retry
We will introduce retry
briefly, but it is unlikely that you will use it often. Using retry
in your begin
/rescue
block redirects your program back to the begin
statement. This allows your program to make another attempt to execute the code that raised an exception. You may find retry
useful when connecting to a remote server, for example. Beware that if your code continually fails, you risk ending up in an infinite loop. In order to avoid this, it's a good idea to set a limit on the number of times you want retry
to execute. retry
must be called within the rescue
block, as seen below on line 8. Using retry
elsewhere will raise a SyntaxError
.
RETRY_LIMIT = 5begin
attempts = attempts || 0
# do something
rescue
attempts += 1
retry if attempts < RETRY_LIMIT
end
Raising Exceptions Manually
So far, this article has discussed how to handle exceptions raised by Ruby. In the previous code examples, we have had no control over when to raise an exception or which error type to use; it has all been decided for us. Handling an exception is a reaction to an exception that has already been raised.
Now, let’s switch gears and explore how you can exert more control when working with exceptions in a program. Ruby actually gives you the power to manually raise exceptions yourself by calling Kernel#raise. This allows you to choose what type of exception to raise and even set your own error message. If you do not specify what type of exception to raise, Ruby will default to RuntimeError
(a subclass of StandardError
). There are a few different syntax options you may use when working with raise
, as seen below.
raise TypeError.new("Something went wrong!")raise TypeError, "Something went wrong!"
In the following example, the exception type will default to a RuntimeError
, because none other is specified. The error message specified is "invalid age"
.
def validate_age(age)
raise("invalid age") unless (0..105).include?(age)
end
It is important to understand that exceptions you raise manually in your program can be handled in the same manner as exceptions Ruby raises automatically.
begin
validate_age(age)
rescue RuntimeError => e
puts e.message #=> "invalid age"
end
Above, we placed the validate_age
method in a begin
/rescue
block. If an invalid age is passed in to the method, a RuntimeError
with the error message "invalid age"
will be raised and the rescue
clause of our begin
/rescue
block will be executed.
Raising Custom Exceptions
In addition to providing many built-in exception classes, Ruby allows us the flexibility to create our own custom exception classes.
class ValidateAgeError < StandardError; end
Notice that our custom exception class ValidateAgeError
is a subclass of an existing exception. This means that ValidateAgeError
has access to all of the built-in exception object behaviors Ruby provides, including Exception#message
and Exception#backtrace
. As discussed earlier in this article, you should always avoid masking exceptions from the Exception
class itself and other system-level exception classes. Concealing these exceptions is dangerous and will suppress very serious problems in your program-- don't do it. Most often you will want to inherit from StandardError
.
When using a custom exception class, you can be specific about the error your program encountered by giving the class a very descriptive name. Doing so may aid in debugging. Let’s alter our previous code example and use our more descriptive custom exception class.
def validate_age(age)
raise ValidateAgeError, "invalid age" unless (0..105).include?(age)
endbegin
validate_age(age)
rescue ValidateAgeError => e
# take action
end
As demonstrated in the example above, you can raise and handle custom exceptions just like any built-in exception that Ruby provides.
In Conclusion
We hope that this article has left you feeling confident and informed on how to work with exceptions in your Ruby programs. Some of the techniques for error handling described here may be used by you only very rarely, while others may be useful more often. Don’t hestitate to refer back to this article or the documentation for a refresher when you need it.