In the real world, the unexpected happens. Someone could delete the file your program is trying to load, or the server your program is trying to contact could go down. Your code could check for these exceptional situations, but those checks would be mixed in with the code that handles normal operation. (And that would be a big, unreadable mess.)
This chapter will teach you all about Ruby’s exception handling, which lets you write code to handle the unexpected, and keep it separate from your regular code.
There’s always a risk that users will make mistakes when calling methods in your code. Take this simple class to simulate an oven, for example. Users create a new instance, call its turn_on
method, set its contents
attribute to the dish they want to cook (we could only afford a small oven that holds one dish at a time), and then call the bake
method to have their dish cooked to a nice golden brown.
But users might forget to turn the oven on before calling bake
. They could also call bake
while the contents
attribute is set to nil
. So we’ve built in some error handling for both of those scenarios. Instead of returning the cooked food item, it will return an error string.
If we remember to turn the oven on and place food inside, everything works great!
But as we’re about to see, it doesn’t work so well when there’s a problem. Using a method’s return value to indicate an error (as in the code above) can cause more trouble...
So what happens if we forget to put food in the oven? If we accidentally set the oven’s contents
attribute to nil
, our code will “serve” the warning message!
But that’s just one course ruined. It’s worse if we fail to turn the oven on—that could spoil our entire meal!
The real problem here is that when there’s an error, our program keeps running as if nothing is wrong. Fortunately, we learned about a fix for this sort of thing back in Chapter 2...
Remember the raise
method? It halts program execution with an error message when an error is encountered. And it seems a lot safer than method return values to communicate error messages...
Let’s try replacing the error return values in our SmallOven
class with calls to raise
:
Now, if we try to bake something with the oven empty or turned off, instead of getting “served” an error message, we’ll get an actual error...
Before, when we were using return values to report errors in the bake
method, we would sometimes accidentally treat an error message as if it were food.
But we’ll say this for the old program: after serving us an error message, at least it went on to serve us the dessert course as well. Now that we’re using raise
to report errors in the bake
method, our program exits the moment a problem is detected. No pie for us!
And that error message is ugly, too. References to line numbers within your script might be useful to developers, but it will just confuse regular users.
If we’re going to keep using raise
within the bake
method, we’ll have to fix these problems. And to do that, we’ll need to learn about exceptions...
If we call raise
all by itself in a script, we’ll see output like this:
The raise
method is actually creating an exception object, an object that represents an error. If they’re not dealt with, exceptions will stop your program cold.
An exception is an object that represents an error.
Here’s what’s happening:
When you call raise
, you’re saying, “there’s a problem, and we need to stop what we’re doing now.”
The raise
method creates an exception object to represent the error.
If nothing is done about the error, your program will exit, and Ruby will report the error message.
But it’s also possible to rescue an exception: to intercept the error. You can report the error message in a more user-friendly way, or sometimes even fix the problem.
If you have some code that you think might raise exceptions, you can surround it in a begin
/end
block, and add one or more rescue
clauses that will run when an exception is encountered. A rescue
clause may contain code to write an error message to a logfile, reattempt a network connection, or do whatever is needed to deal gracefully with the problem.
If any expression in the begin
block body raises an exception, code execution will immediately move to the appropriate rescue
clause if one is present.
Once the rescue
clause finishes, code execution will continue normally following the begin
/end
block. Since you presumably handled the problem in the rescue
clause, there’s no need for your program to end.
You can raise an exception from your main program (outside of any method), but it’s much more common for exceptions to be raised inside a method. If that happens, Ruby will first look for a rescue
clause within the method. If it doesn’t find one, the method will immediately exit (without a return value).
When the method exits, Ruby will also look for a rescue
clause in the place the method was called. So if you’re calling a method that you think might throw an exception, you can surround the call with a
This can continue through multiple methods. If the method’s caller doesn’t have an appropriate rescue
clause, Ruby will exit that method immediately, and look for a rescue clause in its caller. This continues through the chain of calls until an appropriate rescue
clause is found. (If none is ever found, the program halts.)
Ruby looks for a
rescue
clause in the method where the exception occurred. If none is found there, it looks in the calling method, then in that method’s caller, and so on.
Right now, if we call the bake
instance method of our SmallOven
class without setting the instance’s contents
attribute, we get a user-unfriendly error message. The program also stops immediately, without processing the remaining items in the array.
Let’s add a rescue
clause to see if we can print a more user-friendly error.
Much better! All the items in the array get processed, and we get a readable error message, all without the risks associated with returning an error string from the method!
When there’s an error in the bake
method, we pass a string describing the problem to the raise
method.
We’re not really making use of those messages; instead, we have a single string in the rescue
clause that we always print, saying that the oven is empty.
But if the oven is actually off instead of empty, we’ll print an inaccurate error message!
We need a way to print the message that was passed to raise
instead...
When you pass a string to the raise
method, it uses that string to set the message
attribute of the exception it creates:
All we need to do is remove our static error message, and print the message
attribute of the exception object in its place.
We can store the exception in a variable by adding =>
to the rescue
line, followed by any variable we want. (The =>
is the same symbol used in some hash literals, but in this context it has nothing to do with hashes.) Once we have the exception object, we can print its
Let’s update our oven code to store the exception in a variable, and print its message:
Problem solved. We can now display whatever exception message was passed to the raise
method.
We’ve covered a lot of ground since we started trying to improve the errors from our oven simulator code! Let’s recap the changes we’ve made.
In the SmallOven
class’s bake
method, we added raise
statements that raise an exception when there’s a problem. The exception object’s message
attribute is set differently depending on whether the oven is off or empty.
In our code that calls the bake
method, we’ve set up our rescue
clause to store the exception object in a variable named error
. We then print the exception’s message
attribute to indicate exactly what went wrong.
So if our oven’s contents
attribute gets set to nil
, we’ll see one error message:
...and if the oven is turned off, we’ll see a different message.
It would be nice if our program could detect the problem, switch the oven on, and then attempt to bake the item again.
But we can’t just switch on the oven and try again for any exception we get. If the contents
attribute is set to nil
, there’s no point in trying to bake that a second time!
We need a way to differentiate between the exceptions that the bake
method can raise, so that we know to handle them differently. And we can do that using the exception’s class...
We mentioned before that exceptions are objects. Well, all objects are instances of a class, right? You can specify what exception classes a particular rescue
clause will handle. From then on, that rescue
clause will ignore any exception that isn’t an instance of the specified class (or one of its subclasses).
You can use this feature to route an exception to a rescue
clause that is set up to handle it in the way you want.
But if we’re going to handle different exception classes in different ways, we first need a way to specify what the class of an exception is...
When we call raise
, it creates an exception object...and if nothing rescues the exception, you can see the class of that object when Ruby exits.
By default, raise
creates an instance of the RuntimeError
class. But you can specify another class for raise
to use, if you want. Just pass the class name as the first argument, before the string you want to use as an exception message.
You can even create and raise your own exception classes. You’ll get an error (and not the error you want) if you use just any old class, though:
Only subclasses of Ruby’s Exception
class can be used to represent exceptions. Here’s a partial hierarchy for the exception classes in Ruby’s core library:
So if you make your exception class a subclass of Exception
, it will work with raise
...
...but notice that the majority of Ruby exception classes are subclasses of StandardError
, not Exception
directly. By convention, StandardError
represents the type of errors that a typical program might be able to handle. Other subclasses of Exception
represent problems that are outside of your program’s control, like your system running out of memory or shutting down.
So while you could use Exception
as the superclass for your custom exceptions, you should generally use StandardError
instead.
Now that we can create our own exception classes, we need to be able to rescue the right classes. You can include a class name right after the rescue
keyword in a rescue clause, to specify that it should rescue only exceptions that are instances of that class (or one of its subclasses).
In this code, the raised exception (a PorridgeError
) doesn’t match the type specified in the rescue
clause (BeddingError
), so the exception isn’t rescued:
...but any exceptions that do match the class listed in the rescue
clause will be rescued:
It’s a good idea to always specify an exception type for your rescue
clauses. That way, you’ll only rescue exceptions that you actually know how to handle!
This code will rescue any instance of BeddingError
that gets raised, but it ignores PorridgeError
. We need to be able to rescue both exception types...
You can add multiple rescue
clauses to the same begin
/end
block, each specifying a different type of exception it should rescue.
This allows you to run different recovery code depending on what type of exception was rescued.
Now that we know how to raise our own exception classes, and how to handle different exception classes in different ways, let’s try updating our oven simulator. If the oven is off, we need to turn it on, and if the oven is empty, we need to warn the user.
We’ll create two new exception classes to represent the two types of exceptions that can occur, and make them subclasses of the StandardError
class. Then we’ll add rescue
clauses for each exception class.
It worked! When the cold oven raises an OvenOffError
, the appropriate rescue
clause is invoked, and the oven is turned on. And when the nil
value raises an OvenEmptyError
, the rescue
clause for that exception prints a warning.
We’re missing something here... Our rescue
clause for the OvenOffError
turned the oven back on, and all the remaining items were baked successfully. But because the OvenOffError
occurred while we were trying to bake the turkey, that part of our meal gets skipped! We need a way to go back and reattempt baking the turkey after the oven is turned on.
The retry
keyword should do just what we need. When you include retry
in a rescue
clause, execution returns to the start of the begin
/end
block, and statements there get rerun.
For example, if we encounter an exception because of an attempt to divide by zero, we can change the divisor and try again.
Be cautious when using retry
, however. If you don’t succeed in fixing the issue that caused the exception (or there’s a mistake in your rescue
code), the exception will be raised again, retried again, and so on in an infinite loop! In this event, you’ll need to press Ctrl-C to exit Ruby.
In the above code, if we had included retry
but failed to actually fix the divisor, we’d get an infinite loop:
Let’s try adding retry
to our rescue
clause after we turn the oven on, and see if the turkey gets processed this time:
We did it! Not only were we able to fix the issue that raised an exception, but the retry
clause allowed us to process the item again (successfully this time)!
We’re just now realizing something about all the prior examples: we never turn the oven off when we’re done.
It’s not as simple as adding a single line of code to turn it off, though. This code will leave the oven on, because an exception is raised before
We could add a call to turn_off
in the rescue
clause as well...
...but having to duplicate code like that isn’t ideal, either.
If you have some code that you need to run regardless of whether there’s an exception, you can put it in an ensure
clause. The ensure
clause should appear in a begin
/end
block, after all the rescue
clauses. Any statements between the ensure
and end
keywords are guaranteed to be run before the block exits.
The ensure
clause will run if an exception is raised:
The ensure
clause will run if an exception isn’t raised:
Even if an exception isn’t rescued, the ensure
clause will still run before Ruby exits!
Situations where you need to run some cleanup code whether an operation succeeds or not are pretty common in programming. Files need to be closed even if they’re corrupted. Network connections need to be terminated even if you didn’t get the data. And ovens need to be turned off even if the food’s overcooked. An
Let’s try moving our call to oven_off
to an ensure
clause, and see how it works...
It works! The ensure
clause is called as soon as the begin
/end
block body finishes, and the oven is turned off.
Even if an exception is raised, the oven still gets turned off. The rescue
clause runs first, followed by the ensure
clause, which calls turn_off
.
In the real world, things don’t always go according to plan, and that’s why exceptions exist. It used to be that encountering an exception would bring your program to a screeching halt. But now that you know how to handle them, you’ll find that exceptions are actually a powerful tool for keeping your code running smoothly!
Even programmers make mistakes, which is why testing your programs is so important. But testing everything by hand is time-consuming and, frankly, boring. In the next chapter, we’ll show you a better way: automated tests.
18.227.72.15