Chapter 12. Exceptions: Handling the Unexpected

image with no caption

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.

Don’t use method return values for error messages

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.

image with no caption

If we remember to turn the oven on and place food inside, everything works great!

image with no caption

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!

image with no caption

But that’s just one course ruined. It’s worse if we fail to turn the oven on—that could spoil our entire meal!

image with no caption

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

image with no caption

Using “raise” to report errors

Let’s try replacing the error return values in our SmallOven class with calls to raise:

image with no caption

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

image with no caption

Using “raise” by itself creates new problems

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.

image with no caption

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!

image with no caption

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

Exceptions: When something’s gone wrong

If we call raise all by itself in a script, we’ll see output like this:

image with no caption

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:

  1. When you call raise, you’re saying, “there’s a problem, and we need to stop what we’re doing now.”

  2. The raise method creates an exception object to represent the error.

  3. If nothing is done about the error, your program will exit, and Ruby will report the error message.

image with no caption

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.

Rescue clauses: A chance to 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.

image with no caption

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.

image with no caption

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.

Ruby’s search for a rescue clause

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

image with no caption

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

image with no caption

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

image with no caption

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.

Using a rescue clause with our SmallOven class

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.

image with no caption

Let’s add a rescue clause to see if we can print a more user-friendly error.

image with no caption

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!

We need a description of the problem from its source

When there’s an error in the bake method, we pass a string describing the problem to the raise method.

image with no caption

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!

image with no caption

We need a way to print the message that was passed to raise instead...

Exception messages

When you pass a string to the raise method, it uses that string to set the message attribute of the exception it creates:

image with no caption

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

image with no caption

Let’s update our oven code to store the exception in a variable, and print its message:

image with no caption

Problem solved. We can now display whatever exception message was passed to the raise method.

Our code so far...

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.

image with no caption

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.

image with no caption

So if our oven’s contents attribute gets set to nil, we’ll see one error message:

image with no caption

...and if the oven is turned off, we’ll see a different message.

image with no caption

Different rescue logic for different exceptions

image with no caption

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!

image with no caption

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.

image with no caption

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

Exception classes

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.

image with no caption

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.

image with no caption

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:

image with no caption

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:

image with no caption

So if you make your exception class a subclass of Exception, it will work with raise...

image with no caption

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

image with no caption

Specifying exception class for a rescue clause

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:

image with no caption

...but any exceptions that do match the class listed in the rescue clause will be rescued:

image with no caption

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!

Multiple rescue clauses in one begin/end block

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

image with no caption

You can add multiple rescue clauses to the same begin/end block, each specifying a different type of exception it should rescue.

image with no caption

This allows you to run different recovery code depending on what type of exception was rescued.

image with no caption

Updating our oven code with custom exception classes

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.

image with no caption

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.

Trying again after an error with “retry”

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.

image with no caption

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.

image with no caption

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:

image with no caption

Updating our oven code with “retry”

Let’s try adding retry to our rescue clause after we turn the oven on, and see if the turkey gets processed this time:

image with no caption

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)!

Things you want to do no matter what

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

image with no caption
image with no caption

We could add a call to turn_off in the rescue clause as well...

image with no caption

...but having to duplicate code like that isn’t ideal, either.

The ensure clause

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:

image with no caption

The ensure clause will run if an exception isn’t raised:

image with no caption

Even if an exception isn’t rescued, the ensure clause will still run before Ruby exits!

image with no caption

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

Ensuring the oven gets turned off

Let’s try moving our call to oven_off to an ensure clause, and see how it works...

image with no caption

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.

image with no caption

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!

Your Ruby Toolbox

That’s it for Chapter 12! You’ve added exceptions to your toolbox.

image with no caption

Up Next...

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.218.157.34