LaunchSchool - An Online School for Developers /

Blog

Getting Started With Ruby Exceptions

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

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
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 the Interrupt 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 a def or end 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, and NoMethodError are all common exceptions that are children or grandchildren of the StandardError 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.

1
2
3
4
5
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.

1
2
3
4
5
6
7
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.

1
2
3
4
5
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?

1
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.

1
2
3
4
5
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.

1
2
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.

1
2
3
4
5
6
7
8
9
10
11
12
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.

1
2
3
4
5
6
7
8
9
RETRY_LIMIT = 5

begin
  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.

1
raise TypeError.new("Something went wrong!")
1
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".

1
2
3
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.

1
2
3
4
5
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.

1
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.

1
2
3
4
5
6
7
8
9
def validate_age(age)
  raise ValidateAgeError, "invalid age" unless (0..105).include?(age)
end

begin
  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.

Ruby