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.
1 2 3
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 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
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-cto exit out of a program? Doing so actually raises an exception via the
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 a
endoff of a method definition.
SystemStackErroris 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.
StandardErrorhas many recognizable descendents.
NoMethodErrorare all common exceptions that are children or grandchildren of the
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
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
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.
1 2 3 4 5
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.
1 2 3 4 5 6 7
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.
1 2 3 4 5
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
So how do we use an exception object?
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#backtrace, which return an error message and a backtrace associated with the exception, respectively. Let’s look at an example of this in a
1 2 3 4 5
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
Recall that exception objects are just normal Ruby objects and the different exception types, such as
NoMethodError, are actually class names. Therefore, we can even call
Object#class on an exception object to return its class name.
You may also choose to include an
ensure clause in your
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.
1 2 3 4 5 6 7 8 9 10 11 12
If there are multiple
rescue clauses in the
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
rescue block will be masked and debugging can become very difficult.
We will introduce
retry briefly, but it is unlikely that you will use it often. Using
retry in your
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
1 2 3 4 5 6 7 8 9
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.
In the following example, the exception type will default to a
RuntimeError, because none other is specified. The error message specified is
1 2 3
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.
1 2 3 4 5
Above, we placed the
validate_age method in a
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
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.
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#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
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.
1 2 3 4 5 6 7 8 9
As demonstrated in the example above, you can raise and handle custom exceptions just like any built-in exception that Ruby provides.
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.