This was a chapter that I decided to include at the last minute. It's not complete but at least you can be aware of basic exception handling.
No matter how carefully you code your script, your program is prone to failure for reasons beyond your control. A website that your script scrapes may suddenly be down. Or someone sharing the same hard drive may delete a file your program is supposed to read from.
Circumstances such as these will crash your program. For any kind of long continuous task that you don't want to baby-sit and manually restart, you will need to write some exception-handling code to tell the program how to carry on when things go wrong.
Demonstrating exceptions
Before the formal description of the the begin/rescue block, let's walk through a couple examples of it in action. At a skin-deep level, it behaves nearly the same as the if/else construct.
Skipping past an error
The Exception class handles nearly every kind of hiccup that might occur during runtime, including syntax screwups and incorrect type handling.
We learned early on that adding numbers and strings with no type conversion would crash a program:
a = 10
b = "42"
a + b
The attempted arithmetic results in this error:
The begin/rescue block is typically used on code in which you anticipate errors. There's only one line here for us to worry about:
a = 10
b = "42"
begin
a + b
rescue
puts "Could not add variables a (#{a.class}) and b (#{b.class})"
else
puts "a + b is #{a + b}"
end
Executing the revised code gets us this:
Two obvious differences from the first try: The puts statement in the rescue clause executed. And more importantly, the Ruby program did not crash.
Let's feed this simple operation with an array of values of different types to see how the else clause comes into play:
values = [42, 'a', 'r', 9, 5, 10022, 8.7, "sharon", "Libya", "Mars", "12", 98, rand + rand, {:dog=>'cat'}, 100, nil, 200.0000, Object, 680, 3.14, "Steve", 78, "Argo"].shuffle
while values.length > 0
a = values.pop
b = values.pop
begin
a + b
rescue
puts "Could not add variables a (#{a.class}) and b (#{b.class})"
else
puts "a + b is #{a + b}"
end
end
With user input
This next demonstration shows how exception-handling can be used in an application in which you accept user input.
For this section, you will have to go to your command line to run it; it won't work from your text-editor. You can refer back to the installation chapter if you've forgotten how to do this.
The steps:
- Open an empty text file and enter a "Hello world" script into it.
- Save it in a directory that's easy to get to. Something like ~/Documents/extest.rb
- Open up your command prompt
- Navigate to the directory of the file: cd ~/Documents
- Run the Ruby interpreter (not irb):
ruby ex-test.rb
The command line should look something like this:
Save the following script into a file and execute it from the command line:
while 1
puts "Enter a number>>"
num = Kernel.gets.match(/\d+/)[0]
puts "#{num} + 1 is: #{num+1}"
end
You should immediately recognize that this script consists of an infinite loop, as while 1 always evaluates to true (remember to press Ctrl-C if you find yourself in a program that won't quit). However, the interpreter doesn't go in a frenzy because it only moves forward after the user enters input, thanks to the Kernel.gets method.
Yet no matter what you type in, you should get an error. Typing in the number 6, for example, will net you this:
~ :) ruby extest.rb Enter a number 6 extest.rb:4:in `+': can't convert Fixnum into String (TypeError) from extest.rb:4
So there's a TypeError in line 4; the fix is obvious. Change:
puts "#{num} + 1 is: #{num+1}"
To:
puts "#{num} + 1 is: #{num.to_i+1}"
Now run extest.rb:
~ :) ruby extest.rb Enter a number>> 6 6 + 1 is: 7 Enter a number>>
This goes on and on. Unless you break it, of course. Type in a non-number:
~ :) ruby extest.rb Enter a number>> No way extest.rb:3: undefined method `[]' for nil:NilClass (NoMethodError)
If a user does not enter a number, the match method in line 3 will return nil, which causes the program to crash out. We can protect against user disobedience by sanitizing the input, of course. The following alteration will convert any non-numerical input to 0:
while 1
puts "Enter a number>>"
num = Kernel.gets.to_i.to_s.match(/\d+/)[0]
puts "#{num} + 1 is: #{num.to_i+1}"
end
~ :) ruby extest.rb Enter a number>> Yo 0 + 1 is: 1 Enter a number>>
The program is happy now. But why should we have to compromise just because the user ignores simple instructions? Sometimes it's OK to go along and compensate for user error. Other times, it's critical to acknowledge the error and yet carry on.
This is where the begin/rescue block comes in:
while 1
puts "Enter a number>>"
begin
num = Kernel.gets.match(/\d+/)[0]
rescue
puts "Erroneous input! Try again..."
else
puts "#{num} + 1 is: #{num.to_i+1}"
end
end
The resulting output:
~ :) ruby extest.rb
Enter a number>>
8
8 + 1 is: 9
Enter a number>>
eight
Erroneous input! Try again...
Enter a number>>
8
8 + 1 is: 9
Enter a number>>
If a failure occurs, the program enters the rescue branch of code; else, the program goes on as normal. We now have a program that both:
- Notifies the user of the existence of an error
- Does not simply crash out because of the error
Like an if/else
There doesn't seem to be much difference between begin/rescue/else and a simpler if/else construct, does there?
while 1
puts "Enter a number>>"
if num = Kernel.gets.match(/\d+/)
num = num[0]
puts "#{num} + 1 is: #{num.to_i+1}"
else
puts "Erroneous input! Try again..."
end
end
At this point, no. This example was only meant to show how exception-handling happens in practice. The rest of this chapter will show how exception-handling will allow you to have finer-grain response to unpredictable runtime problems.
The Begin...Rescue block
This is the most basic error handling technique. It starts off with the keyword begin and acts in similar fashion to an if statement in that it your program flows to an alternate branch if an error is encountered.
The main idea is to wrap any part of the program that could fail in this block. Commands that work with outside input, such as downloading a webpage or making calculation something based from user input, are points of failure. Something like puts "hello world" or 1 + 1 is not.
require 'open-uri'
require 'timeout'
remote_base_url = "http://en.wikipedia.org/wiki"
start_year = 1900
end_year = 2000
(start_year..end_year).each do |yr|
begin
rpage = open("#{remote_base_url}/#{yr}")
rescue StandardError=>e
puts "Error: #{e}"
else
rdata = rpage.read
ensure
puts "sleeping"
sleep 5
end
if rdata
File.open("copy-of-#{yr}.html", "w"){|f| f.write(rdata) }
end
end
- begin
- This starts off the exception-handling block. Put in the operation(s) that is at risk of failing in this clause. In the above example, the open method for retrieving the webpage will throw an exception if the website is down. (ruby-doc definition)
- rescue StandardError=>e
-
This is the branch that executes if an exception or error is raised. Possible exceptions include: the website is down, or that it times out during a request. The rescue clause includes the code we want to execute in the event of an error or exception (there's a difference between the Ruby Exception and Error classes, which I will get to in a later revision).
In this particular rescue clause, I specify that we want this branch of code to execute when a StandardError (Ruby errors have their own classes) occurs. The actual error object will be stored in the variable named e
In this example, the rescue clause only executes a puts statement, printing out the contents of e
- else
- If all goes well, this is where the program branches to. In this example, we save the contents of the open method to a variable. (ruby-doc definition)
- ensure
- This branch will execute whether an error/exception was rescued or not. Here, we've decided to sleep for 3 seconds no matter the outcome of the open method. (ruby-doc definition)
Note: The word retry may be unfamiliar to you. Nonetheless, you can guess what it does here. I cover it formally later in this chapter.
Flow of exception handling
Exception handling is a powerful mechanism in programming. And like all powerful features, the correct and incorrect use of it will have large effects on how reliable and maintainable your script is.
Using retry
The retry statement redirects the program back to the begin statement. This is helpful if your begin/rescue block is inside a loop and you want to retry the same command and parameters that previously resulted in failure.
Here's a simple example; I use the raise statement to create my own Exception to be caught:
for i in 'A'..'C'
retries = 2
begin
puts "Executing command #{i}"
raise "Exception: #{i}"
rescue Exception=>e
puts "\tCaught: #{e}"
if retries > 0
puts "\tTrying #{retries} more times\n"
retries -= 1
sleep 2
retry
end
end
end
The output:
Executing command A Caught: Exception: A Trying 2 more times Executing command A Caught: Exception: A Trying 1 more times Executing command A Caught: Exception: A Executing command B Caught: Exception: B Trying 2 more times Executing command B Caught: Exception: B Trying 1 more times Executing command B Caught: Exception: B Executing command C Caught: Exception: C Trying 2 more times Executing command C Caught: Exception: C Trying 1 more times Executing command C Caught: Exception: C
Using retry with OpenURI
The following snippet of code attempts to download pages from Wikipedia. The third entry, xj3490, refers to a non-existent page and is guaranteed to fail:
require 'open-uri'
remote_base_url = "http://en.wikipedia.org/wiki"
[1900, 1910, 'xj3490', 2000].each do |yr|
retries = 3
begin
url = "#{remote_base_url}/#{yr}"
puts "Getting page #{url}"
rpage = open(url)
rescue StandardError=>e
puts "\tError: #{e}"
if retries > 0
puts "\tTrying #{retries} more times"
retries -= 1
sleep 1
retry
else
puts "\t\tCan't get #{yr}, so moving on"
end
else
puts "\tGot page for #{yr}"
ensure
puts "Ensure branch; sleeping"
sleep 1
end
end
The output is:
Getting page http://en.wikipedia.org/wiki/1900
Got page for 1900
Ensure branch; sleeping
Getting page http://en.wikipedia.org/wiki/1910
Got page for 1910
Ensure branch; sleeping
Getting page http://en.wikipedia.org/wiki/xj3490
Error: 403 Forbidden
Trying 3 more times
Getting page http://en.wikipedia.org/wiki/xj3490
Error: 403 Forbidden
Trying 2 more times
Getting page http://en.wikipedia.org/wiki/xj3490
Error: 403 Forbidden
Trying 1 more times
Getting page http://en.wikipedia.org/wiki/xj3490
Error: 403 Forbidden
Can't get xj3490, so moving on
Ensure branch; sleeping
Getting page http://en.wikipedia.org/wiki/2000
Got page for 2000
Ensure branch; sleeping
As you can see in the highlighted code above, the ensure branch is skipped by the retry. The retry statement can be very useful but because of the "jump" it creates in your program flow, take care in using it so that your script isn't difficult to understand. And of course, if you don't have some kind of limiting condition, such as retries > 0 – just a simple decrementing variable I set up for this script – your script will end up in an infinite loop.
Exception and Error Classes
Not all errors are the same. And so when designing your exception handling blocks, you may find it necessary to write rescue statements for specific errors, rather than just a catch-all rescue statement as we've done so far.
This section will make more sense if you have a little understanding of object-oriented programming. If you don't have time to read the chapter on it, the basic concept as it relates to exceptions and errors is this:
- Every type of error and exception is derived from the Exception class
- If your code rescues a StandardError, it will only rescue errors that are derived from StandardError.
- If your code rescues an Exception, it will basically handle every possible error that could happen, including all errors of StandardError type and its children types.
You can see the family tree of Exception here.
Example: Casting a wide rescue-net for exceptions
Let's return to the chapter's opening example, but slightly altered to print out the type of error. Remember that you must run it from the command line:
while 1
puts "Enter a number>>"
begin
num = Kernel.gets.match(/\d+/)[0]
rescue StandardError=>e
puts "Erroneous input!"
puts e
puts "\tTry again...\n"
else
puts "#{num} + 1 is: #{num.to_i+1}"
end
end
The output:
~ :) ruby extest.rb Enter a number>> 5 5 + 1 is: 6 Enter a number>> a Erroneous input! undefined method `[]' for nil:NilClass Try again... Enter a number>>
Run the script but use Ctrl-C to break out of it. You should see something like this as you are kicked out to the command prompt
Enter a number>> ^Cextest.rb:4:in `gets': Interrupt from extest.rb:4 ~ :)
Instead of rescuing StandardError – which is the default class of error that is rescued if you don't specify otherwise) – modify the code so that it will rescue the Exception class:
while 1
puts "Enter a number>>"
begin
num = Kernel.gets.match(/\d+/)[0]
rescue Exception=>e
puts "Erroneous input!"
puts e
puts "\tTry again...\n"
else
puts "#{num} + 1 is: #{num.to_i+1}"
end
end
Run it from the command line. It'll execute in the same way as it did before. However, when you try Ctrl-C to break out of the program, you'll find that it won't let you:
~ :) ruby extest.rb
Enter a number>>
7
7 + 1 is: 8
Enter a number>>
a
Erroneous input!
undefined method `[]' for nil:NilClass
Try again...
Enter a number>>
^CErroneous input!
Try again...
Enter a number>>
^CErroneous input!
Try again...
Enter a number>>
Highlighted in red is where I've attempted to break out of the program. Why not?
Unfortunately it won't print out the type of exception, but what's happening is that Ctrl-C creates an Interrupt-type exception. But because our program is designed to rescue Exception, which includes Interrupt, the program "rescues" our Ctrl-C action. Thus, we can't use that to break out of the program (you'll just have to shut down your command line window to get out of it.)
The main lesson here is that while it may be convenient to rescue everything, it may cause unwanted effects and behavior. Be specific when possible.
In the next section, we'll examine the Exception family tree.
The Exception family tree
Here's a handy family tree of Exception and all of its children errors. This was generated using this handy code snippet from Nick Sieger.
Exception NoMemoryError ScriptError LoadError NotImplementedError SyntaxError SignalException Interrupt StandardError ArgumentError IOError EOFError IndexError StopIteration LocalJumpError NameError NoMethodError RangeError FloatDomainError RegexpError RuntimeError SecurityError SystemCallError SystemStackError ThreadError TypeError ZeroDivisionError SystemExit fatal
As you can see, the StandardError class covers just about any kind of syntax-type error. For example, if your code tries to read from a file that doesn't exist:
Oops: No such file or directory - somefilethatdoesntactuallyexist.txt
(Errno::ENOENT)
The output:
Because the attempt to read a non-existing file causes an error in the operating system, Ruby has a special object called Errno to interpret the operating system-specific code. In this case, that operating system-specific code is ENOENT, and the error message is "No such file or directory". This all falls under SystemCallError