Exceptions - Effective Ruby: 48 Specific Ways to Write Better Ruby (Effective Software Development Series) (2015)

Effective Ruby: 48 Specific Ways to Write Better Ruby (Effective Software Development Series) (2015)

4. Exceptions

Sometimes an error condition is bad enough that it seems flat out rude to return nil from a method. When things get this mucked up it’s best to unwind the stack and provide details describing the failure.

Purists (of which I’m a card carrying member) believe that exceptions should rarely be used. Mostly to give a meaningful apology to the user. (Followed promptly by crashing the program and discarding all her hard work.)

Ruby programmers tend to be more liberal with exceptions. A number of routine error conditions are encoded into standard exception classes and used to report specific failures. Fortunately, the most common exception classes contain enough information that it’s practical to rescue the exception and recover from the error condition.

Exceptions and exception handling are a complicated business. They interrupt the normal control flow of a program, acting like a sort of broken time machine. They’re computationally expensive, and they’re mentally expensive for programmers. “Where’s that exception going to end up?”

My goal for this chapter is to show you how to avoid common problems with exceptions and how to use them properly. More importantly, you’ll learn to write code that can safely coexist in the presence of exceptions.

Item 22: Prefer Custom Exceptions to Raising Strings

Suppose you’re writing a library and a particular method might encounter an error condition. After thinking on it for a great deal of time you’ve decided that this error warrants an exception. Which exception do you raise? What details do you include in the exception?

Exceptions can be thought of as two different language features rolled into one, error descriptions and control flow. This item is concerned with the former while the latter is covered in Items 26 and 27.

Primarily, an exception is an object which contains information about an error condition. Most often that information is simply a human readable description of the error which is printed to the terminal just before the program crashes. But, as you know, that’s not the only thing you can do with exceptions. They can be intercepted and handled with a rescue clause.

Certain types of exceptions represent errors which can be recovered from. Sometimes that means you abort the current task and log an error message. More detailed exceptions might allow you to change the state of the program and try the operation again. A network timeout, for example, might raise an exception which would trigger a short delay followed by a retry.

In order to recover from a failure you need to be able to differentiate the various possible error conditions. Unfortunately, Ruby makes it too easy to be lazy with exceptions. A much too common practice among Ruby programmers is to use raise with a String:

raise("coffee machine low on water")

Recall that the raise method in Ruby comes in several flavors. When given a single argument, and that argument is a string, raise will create a RuntimeError using the string as the error message. It’s equivalent to the more explicit:

raise(RuntimeError, "coffee machine low on water")

As you can see, instead of using a string you can supply a class name as the first argument to raise. In this case it will create an exception object from the class and then raise that object. The second (and optional) argument is the string to use as the error message. Without an explicit error message the name of the class will be used. (Which isn’t very useful as far as error messages go.)

Raising a RuntimeError might be fine if your intention is to terminate the program. For anything else it isn’t very useful. A RuntimeError is nothing more than a generic “Oops, something went wrong” error. Remembering that you’re writing code for other programmers to use, a RuntimeError is a poor choice, and by extension so is raising a string.

If you’re bound and determined to raise an exception then you should create a new class for it. Since exceptions are handled based on their type, creating a new class is the standard way to differentiate it.

Creating a new exception class is trivial. You only need to consider three rules:

New exception classes must inherit from one of the standard exception classes. Attempting to raise anything else will result in a TypeError. (Discarding the exception you were trying to raise in the first place.)

The standard exception classes form a hierarchy with the Exception base class as the root. However, Exception and several of its sub-classes are considered low-level errors which should generally crash the program. The majority of the standard exception classes inherit instead from StandardError and you should follow suit.

While not a technically requirement, it’s common practice to give exception class names the “Error” suffix.

Another reason for inheriting from StandardError comes from the default behavior of the rescue clause. As you know, you can omit the class name when handling exceptions with rescue. In this case it will intercept any exception whose class (or superclass) is StandardError. (The same is true when you use rescue as a statement modifier.)

Custom exception classes can be as simple or as complicated as you’d like. They’re just classes after all. Let’s start with something simple (the more common case) and work our way up to a slightly more complicated situation. As recommended, we’ll create a class which inherits from StandardError. Not surprisingly, all the functionality we need is already baked in to the Exception class and passed down to our new class for free when we inherit from StandardError:

class CoffeeTooWeakError < StandardError; end

With a custom exception class, errors can now be uniquely identified. People using your code gain the ability to choose how they want to handle (or not handle) these types of errors. You’ve made the world a slightly better place and I congratulate you. But stay with me for a few more paragraphs, you still need to put this hard work into action and actually use this new exception class. Doing so is exactly like using any other exception:

raise(CoffeeTooWeakError)

Preferably you’d supply an error message to go along with the exception. Otherwise a rather useless message will show up on the console if the program ends in a fiery death. A little extra information can go a long way:

raise(CoffeeTooWeakError, "coffee to water ratio too low")

And there you have it. This short, two line enhancement to your project provides a wealth of information for other programmers. Can you imagine going back to simply raising strings? I sure hope not.

Now that I’ve got you hooked on customizing exceptions you’re probably wondering how you can make them even more useful. Maybe you’d like to provide more detail than just an error message. Perhaps some additional information attached to the exception would help programmers using your code decide how to proceed when handling these exceptions. Adding that additional information is simple, remember these are just plain Ruby classes. Let’s create another exception class, this time demonstrating how to report this extra information along with the error message.

Suppose you’re working on a utility which drives the operation of a 3D printer. The temperature of the printer needs to be within a specific range otherwise it won’t function properly. You’ve decided that if the temperature is out of range you’ll raise an exception, but you don’t want to report the current temperature buried somewhere within an error message. It would be nice if the temperature could be programmatically extracted from the exception. The code handling the exception could then change the color of the error message on the printer’s screen to reflect if it’s too cold or too hot.

Writing the custom exception class is straight forward. We’ll capture the temperature in an instance variable and then generate an error message for the exception using the temperature value:

class TemperatureError < StandardError
attr_reader(:temperature)

def initialize (temperature)
@temperature = temperature
super("invalid temperature: #@temperature")
end
end

Nothing really special here. Just remember to initialize the base class with a call to super so the upstream Exception class can set up some internal instance variables, including the error message. As before, the next step is to put our new exception class to work. This time, however, things are slightly different:

raise(TemperatureError.new(180))

Instead of using a class name with raise we’re giving it an exception object. Clearly we need to create the exception object ourselves in order to pass in the temperature, so giving raise a class name isn’t an option. Fortunately raiseis smart enough to accept either a class or an object. Ignoring the special case of raise where you give it a string, it’s worthwhile to briefly explore how it creates an exception from the object you give it.

The raise method doesn’t do any voodoo or introspection with its first argument, it simply sends it a message, the exception message to be more specific. This method is suppose to return an object which can then be raised. Both exception classes and exception objects have their own exception method courtesy of the Exception class. The class method version is simply an alias for new and isn’t very interesting. Passing a class to raise causes it to therefore call new on the class (via the exception method) and raise the resulting object.

The instance method version of exception is a bit weird though, depending on how many arguments are given to raise:

In the single argument version of raise (the one we used above) the exception method will be invoked with no arguments. With no arguments the exception method simply returns self without doing anything. This has the effect that raise will end up raising the object you gave it as its first argument.

If, however, you provide raise with an exception object and an error message it will invoke the exception method with a single argument, the error message. In this case the exception method will make a copy of the exception, reset its error message to the one given, and then return the copy. This results in the copy (along with the new error message) being raised instead.

Keep this second case in mind if you’re setting the error message in the initialize method like we did above. Giving raise one of our TemperatureError objects and an error message will result in the error message from initializebeing overwritten. When working with exceptions that generate their own error messages you should use the single argument version of raise instead. (Unless, of course, your goal is to actually overwrite the error message.)

Armed with the rules for creating custom exception classes you’re ready to make all of your errors more descriptive and useful. But before I turn you loose I’d like to make one final recommendation.

If you find yourself creating several exception classes for a single project you should consider arranging them in their own class hierarchy. That is, designate one of them as a base class which the remaining classes inherit from. The base class should of course inherit from StandardError. Structuring your exception classes this way gives other programmers even more flexibility when handling your exceptions.

Things to Remember

Avoid raising strings as exceptions, they’re converted into generic RuntimeError objects. Create a custom exception class instead.

Custom exception classes should inherit from StandardError and use the “Error” suffix in their class name.

When creating more than one exception class for a project, start by creating a base class which inherits from StandardError. Other exception classes should inherit from this custom base class.

If you write an initialize method for your custom exception class make sure to call super, preferably with an error message.

When setting an error message in initialize, keep in mind that setting an error message with raise will take precedence over the one in initialize.

Item 23: Rescue the Most Specific Exception Possible

So you’re working on a critical piece of software that needs to have the minimum amount of downtime. Since it runs in the real world it’s not uncommon for the application to experience a variety of errors. Everything between network connection failures and corrupt data in the database conspire against your plan to write a robust program. You therefore decide to handle exceptions as much as possible instead of letting the program crash. Because different exceptions might be raised while performing a crucial task, it seems that wrapping the task in a begin block is the obvious way to go:

begin
task.perform
rescue => e
logger.error("task failed: #{e}")
# Swallow exception, abort task.
end

Realizing that using rescue without a class name is a rather large net, you decide to at least log the error message before swallowing the exception. But just how wide is this net?

As you know, using rescue like this is a shortcut for rescuing StandardError and any of its sub-classes. The problem is, this just happens to be the majority of the exception classes. These are the so-called “high-level” exceptions which are safe to handle and sometimes ignore. Among them, however, are exceptions related to your source code as well. Exceptions like LocalJumpError and ArgumentError aren’t environmental issues like the network or file system—they’re errors caused by mistakes in the code.

Perhaps they’re mistakes such as forgetting to validate a user entry before using it, but they’re mistakes nonetheless. You really don’t want to cover these sorts of errors up by grouping them with other StandardError exceptions. You also don’t want to specifically list them all out and deal with them individually. You can think of it as a blacklist vs. whitelist issue.

Blacklisting is inclusive, everything is allowed unless specifically denied. Dealing with exceptions this way would have you first listing out all the exceptions you don’t want to handle, simply re-raising them immediately after being rescued. The final rescue clause is then your “everything else” bucket. Obviously, this is a nightmare:

begin
task.perform
rescue ArgumentError, LocalJumpError,
NoMethodError, ZeroDivisionError
# Don't actually handle these.
raise
rescue => e
logger.error("task failed: #{e}")
# Swallow exception, abort task.
end

If you follow the blacklist pattern you need to list the exceptions out like this because Ruby evaluates the rescue clauses in order, from top to bottom, first match wins. That means if you rescue StandardError before LocalJumpErrorthe more generic (higher in the class hierarchy) StandardError would always win. Furthermore, the code no longer clearly represents your intention. Someone working on this code in a few months may see these rescue statements as redundant and remove them. In other words, this style is too brittle and error-prone. What do you do?

A more natural approach would be to rescue only those specific exceptions you know how to handle and let the rest propagate up the stack. This is closer to whitelisting where everything is denied unless specifically allowed (exclusive). This method requires you to spend more time up front considering potential error conditions but with the benefit that they are handled with specific solutions.

Suppose you know that the two most common errors your code will encounter are going to be network connection failures and invalid database records. Connection failures can be handled by retrying after a delay for a set number of times before giving up. (Make sure you take heed to the advice in Item 26 when using the retry statement.) The bad records, on the other hand, need to be flagged and sent off to support staff.

Item 22 recommends that you have specific exception classes for these two types of errors and this is precisely the reason why:

begin
task.perform
rescue NetworkConnectionError => e
# Retry logic...
rescue InvalidRecordError => e
# Send record to support staff...
end

With dedicated exception classes it’s straight forward to rescue specific errors and deal with them individually. This way the common error conditions are taken care of and the generic (and hopefully rare) errors continue on their way towards eventually crashing the program.

If you still feel like you want that catchall bucket at the end of the rescue chain you might actually be itching for an ensure clause. This is especially true if you just want to perform some housecleaning before a raised exception causes the current scope to exit. Since the ensure clause will execute for both normal and exceptional situations it’s the preferred way to clean up. Items 24 and 25 provide some recommendations for using ensure correctly.

Still want to use a general purpose rescue? Okay, I’ll admit that you might occasionally have a genuine reason to rescue generic exceptions. For example, when you want to send the details of the exception to a remote monitoring service before re-raising it:

begin
task.perform
rescue NetworkConnectionError => e
# Retry logic...
rescue InvalidRecordError => e
# Send record to support staff...
rescue => e
service.record(e)
raise
ensure
...
end

I’d be remiss if I didn’t mention a not so insignificant detail about rescuing exceptions. While it’s completely admirable and highly recommended to handle recoverable exceptions, it’s not without its fair share of risk. Consider the example above where invalid database records trigger exceptions which are subsequently rescued and then sent off to the support team. Suppose this process involves making a network connection to the support system and transmitting the details of the invalid record. What happens if there’s a network connection failure while connecting to this other system? Here’s a hint: it’s not pretty.

Exceptions raised while a rescue clause is executing replace the original exception, exit the scope of the rescue clause, and start exception processing with the new exception. This isn’t ideal since the original reason you were handling an exception has been lost and instead some other exception is parading around in its place. This is especially egregious if the lost exception is something like LocalJumpError which was accidentally rescued by a generic rescue clause such as the one above which sends exceptions to some reporting service. Ouch.

This can be mitigated by performing any recovery work in a dedicated method which receives the original exception as an argument. Any exceptions which occur in the method can be handled by raising the original exception:

def send_to_support_staff (e)
...
rescue
raise(e)
end

Of course this assumes the original exception is more important than the exception raised during the recovery process. Going to such lengths is a call you’ll have to make on your own.

Things to Remember

Rescue only those specific exceptions from which you know how to recover.

When rescuing an exception, handle the most specific type first. The higher a class is in the exception hierarchy the lower it should be in your chain of rescue clauses.

Avoid rescuing generic exception classes such as StandardError. If you find yourself doing this you should consider if what you really want instead is an ensure clause.

Raising an exception from a rescue clause will replace the current exception and immediately leave the current scope, resuming exception processing.

Item 24: Manage Resources with Blocks and ensure

As you know, Ruby includes a garbage collector so you can be a happy programmer and avoid manual memory management. For example, you can slurp a large file into memory without worrying that a raised exception will result in a memory leak.

“But what about other types of resources besides memory?” you ask. That’s an excellent question. I’m glad you’re thinking ahead.

There are a variety of resources that are not handled by the garbage collector, yet like memory, they make use of the allocate-use-release pattern. Database connections, temporary files, and file locks are all good examples of resources that must eventually be freed.

The idea of releasing a resource when you’ve finished with it seems easy enough. Exceptions, however, are the ultimate monkey wrench when it comes to resource management. Consider this code:

file = File.open(file_name, 'w')
...
file.close

What happens when an exception is raised before the file is closed? The good news is that the File class leverages the garbage collector as described in Item 45, so the file will eventually be closed. The bad news is the “eventually” part.

If you rely on the garbage collector you should know that there are no guarantees when a resource will be released, only that it will happen sometime in the future. (And possibly not at all if you accidentally maintain a reference to the resource.) While the garbage collector is great at what it does, most resources need to be released in a timely fashion.

When working with files there is a limit on the maximum number which can be concurrently open. Depending on the operating system this upper limit might be shared with network connections as well. Therefore it’s clearly more desirable to close the file immediately after you’re done with it than wait for the garbage collector. Fortunately there’s a better way to manage resources and it works even if exceptions are raised.

The trick is to use the begin keyword to create a new scope for exceptions. You can then add an ensure clause to release the resource whether or not any exceptions are raised.

The beauty of the ensure clause is that its body will be executed for both normal and exceptional situations. An exception-safe way to use a file isn’t much more complicated than what we’ve seen before:

begin
file = File.open(file_name, 'w')
...
ensure
file.close if file
end

If an exception is raised anywhere in the body of the begin block then the file will be closed by the ensure clause. Since the ensure clause also executes when the begin body finishes successfully, you know that the file will be closed in all possible outcomes.

In addition, expressions in the ensure clause are executed within the same scope as the begin body. Variables defined there are also available in the ensure clause. Exercise caution though, it’s possible that they haven’t yet been initialized. That’s the reason why I’ve made the call to file.close conditional.

It’s possible that File.open failed and raised an exception. In such a case the ensure clause will still run but there’s no need to close a file which was never opened. In an ensure clause it’s a good idea to check if a variable is initialized before attempting to use it. Uninitialized variables in this context will always be nil.

While this code fits the bill for safely managing resources in the presence of exceptions, it’s a bit cumbersome to write repeatedly. Ideally we’d like to be able to abstract away this responsibility.

As it just so happens there’s a design idiom in Ruby that makes resource management fairly painless. To see an example of this idiom in practice we don’t have to look any further than File.open:

File.open(file_name, 'w') do |file|
...
end

Given a block, the File.open class method opens the file as it did before. This time, however, the file will be yielded to the block and when the block finishes the file will be closed. The file is only open as long as it’s needed.

As you probably guessed, the file is closed using an ensure clause. Therefore if the body of the block raises an exception, the file will still be closed.

Writing code in this style is fairly straight forward. If a method is associated with a block you can use the yield statement to invoke the block and pass in the newly acquired resource.’

class Lock
def self.acquire
lock = new # Initialize the resource
lock.exclusive_lock!
yield(lock) # Give it to the block
ensure
# Make sure it gets unlocked.
lock.unlock if lock
end
end

Using this class is equivalent to the File example:

Lock.acquire do |lock|
# Raising an exception here is okay.
end

I’m sure you’d agree that it would be nicer if Lock.acquire could be used exactly like File.open. That is, if given a block the method will own the resource and ensure it’s released. Without a block it simply returns the resource and the caller is responsible for sending the unlock message. (Hopefully employing ensure in the process.)

We only need one more helper method to make this work. The block_given? method returns true if a block is associated with the method. With this in hand we can extend Lock.acquire to behave in the same way that File.opendoes:

class Lock
def self.acquire
lock = new # Initialize the resource
lock.exclusive_lock!
yield(lock) # Give it to the block
ensure
# Make sure it gets unlocked.
lock.unlock if lock
end
end

Without an associated block, Lock.acquire will now simply return the lock and won’t attempt to automatically unlock it.

lock = Lock.acquire
# Won't automatically unlock.

What I really like about this abstraction is that it’s not difficult to write and once it’s in place it performs an important job silently behind the scenes. If I allocate a resource and then leave the current scope through normal flow control or due to an exception, the resource will always be released.

Things to Remember

Write an ensure clause to release any acquired resources.

Use the block and ensure pattern with a class method to abstract away resource management.

Make sure variables are initialized before using them in an ensure clause.

Item 25: Exit ensure Clauses by Flowing off the End

Item 24 makes the point that using an ensure clause is the best way to manage resources in the presence of exceptions. More generally, ensure can be used to perform any sort of housekeeping before leaving the current scope. It’s a fantastic, general purpose cleaning product. Like any useful tool however, ensure comes with a warning label and some sharp edges.

Being closely related to rescue clauses, it should come as no surprise that ensure can fulfill some of the same duties as rescue. Of course, this is specifically limited to when an ensure clause is executing due to a propagating exception. (Which is only one of the reasons it might be executing.)

One of the features that both rescue and ensure share is the ability to terminate exception processing. You already know that rescue catches exceptions. From within rescue you can choose to cancel propagation and deal with the error, resuming normal control flow. You can also restart exception processing by raising the original exception or by creating a new one.

With rescue, terminating exception handling is explicit. The fact that a rescue clause is present alerts you that an exception is going to be dropped or dealt with. On the other hand, ensure gives you warm fuzzies about cleaning up and preventing resource leaks.

You probably don’t expect that an ensure clause can alter control flow and swallow exceptions. That’s certainly not its primary purpose. Nevertheless, it’s possible, fairly simple, and slightly subtle. All it takes is an explicit returnstatement:

def tricky
...
return 'horses'
ensure
return 'ponies'
end

Since there are two reasons why an ensure clause executes, we need to understand how the code above affects both. Let’s first look at the exceptional situation.

Suppose for a moment that from the depths of the call stack an exception is making its way upwards. Wisely, you’ve written an ensure clause to clean up after yourself. Strangely though, you’ve also written a return statement in the ensure clause. Why in the world did you do that?

What will happen when the executing ensure clause reaches the return statement? The exception is discarded and the enclosing method returns as normal. Its return value is replaced with the return value from the ensure clause. This would make perfect sense if it wasn’t absolutely horrible.

“You shouldn’t catch exceptions that you can’t properly handle,” you say. I completely agree with you. That’s what makes this so heinous.

Item 23 advises that you should only rescue the exceptions which you can handle. Returning from ensure directly discards that advice with some unfortunate consequences. Used this way, the ensure clause turns into the Bermuda Triangle for exceptions.

To illustrate how undesirable this is we’ll need to compare it to rescue. Using an explicit return inside ensure is close to writing a bare rescue:

def hammer
...
return 'hit'
rescue
# Discard exceptions.
return 'smash'
end

When you don’t specify which exceptions you intend to intercept you are opting for the default set, which is StandardError and its children. More importantly, it means there are several other exception classes which won’t be caught. That’s a good thing.

There are plenty of exceptions you shouldn’t catch. How would you handle SyntaxError or NoMemoryError? You don’t. You simply clean up and let the exception continue. Hopefully code further up the stack knows what to do with those.

Getting back to that return statement in ensure. The reason it’s worse than a bare rescue is that it will discard any exception. Let me repeat that, it completely throws away any type of exception. It’s a pretty big hammer. Avoid using it.

Instead, clearly show your intent with a proper rescue:

def explicit
...
return 'horses'
rescue SpecificError => e
# Recover from the exception.
return 'ponies'
ensure
# Clean up only.
end

This is probably what you meant to write anyways. Furthermore, the code above doesn’t suffer from the problems created when ensure is executing due to normal (non-exceptional) flow. If you’ve been keeping track you’ll notice we haven’t covered that situation yet.

Take another look at the problem code from the start of this item:

def tricky
...
return 'horses'
ensure
return 'ponies'
end

This time let’s consider what happens when no exceptions are raised. You might call this the “happy path”, the method finishes successfully and returns a value. This is the second reason an ensure clause will run.

Do you remember that pesky return statement you wrote into the ensure clause? When it executes this time it simply takes over the return value of the method. From the outside it appears that the method returned normally and with the value from the ensure clause. Talk about obfuscation.

Notice that for all outcomes this method will always return the value from the ensure clause and never the main body. This is subtly different than the hammer method above. It only returns 'smash' when an exception occurs. Otherwise the body’s return value is maintained.

Even if you think you have a good reason to do something like this, you should reconsider. The same result can be achieved using conditionals in the body of the method or in a rescue clause. It’s safer and more clearly shows your intention.

Up until this point we’ve focused heavily on the return statement. But as you know, it’s not the only way to exit the current scope. There’s an even more powerful keyword than return. The throw statement can be a powerful ally when it comes to control flow. (Item 27 points out why throw should be used instead of raise for control flow.)

Obviously, using throw inside ensure has all the same problems as return. What might not be so obvious are the keywords that alter the flow of iterations and loops. Specifically next and break. Consider what happens when you place them inside ensure:

items.each do |item|
begin
raise TooStrongError if item == 'lilac'
ensure
next # Cancels exception, continues iteration.
end
end

Used this way, next and break can silently discard exceptions. By now we should agree that if you’re going to throw away exceptions it should be done inside a rescue clause where it belongs. (Oh, and you have a really good reason to do so.)

Things to Remember

Avoid using explicit return statements from within ensure clauses. This suggests there’s something wrong with the logic in the method body.

Similarly, don’t use throw directly inside ensure. You probably meant to place the throw in the body of the method.

When iterating, never use next or break in an ensure clause. Ask yourself if you actually need the begin block inside the iteration. It might make more sense to invert the relationship, placing the iteration inside the begin.

More generally, don’t alter control flow in an ensure clause. Do that in a rescue clause instead. Your intent will be much clearer.

Item 26: Bound retry Attempts, Vary Their Frequency, and Keep an Audit Trail

Item 23 explains why it’s important to rescue only those specific exceptions which you know how to handle and how things can go wrong when using rescue. I recommend you familiarize yourself with the ramifications involved when using rescue because this item expands on that idea by demonstrating how to resolve an error condition by rerunning the code which caused it.

Although not very common, certain errors can be safely handled by rescuing the resulting exception and restarting the enclosing scope with the retry keyword. Suppose you’ve written a program which communicates with a third party vendor using a fancy web API. When specific records in your database change your code kicks off and attempts to transmit the changes to the vendor. The only problem is, the vendor is running some very buggy software. 1 out of every 30 requests you make result in a response informing you that your data transfer failed due to a database deadlock error. Good news though, the vendor is aware of the problem and should have it fixed in the next three months.

After some experimenting you discover a simple workaround, retrying the request after a short delay results in a successful transaction. With hundreds of these failures an hour, this task clearly needs to be automated. A natural first attempt might look something like this:

begin
service.update(record)
rescue VendorDeadlockError
sleep(15)
retry
end

There’s a major problem with this code that isn’t immediately obvious. Testing this code—even putting it into production—might work for a bit of time until an unexpected edge case is encountered. The problem lies with how we’re using retry and the fact that it creates a hidden loop in the code. As long as the update method continues to raise a VendorDeadlockError the block will continue to get restarted. Writing an unconditional retry is akin to writing an infinite loop. To make this clearer consider this use of while which emulates our first attempt at using retry:

while true
begin
service.update(record)
rescue VendorDeadlockError
sleep(15)
# Drop exception
else
break # Success, exit loop.
end
end

This is quite a bit uglier but it does make the loop explicit. If we’re going to use retry then we need to take this implicit loop into consideration. One correct solution is to place an upper bound on the number of retry attempts. This is often done with a bounding variable. The tricky part is ensuring that this variable is correctly scoped:

retries = 0

begin
service.update(record)
rescue VendorDeadlockError
raise if retries >= 3
retries += 1
sleep(15)
retry
end

Notice that the retries variable is defined and initialized outside of the begin block. Had it been initialized inside instead it would continually reset to 0 each time retry restarted the begin block, invalidating the bounds checking and creating a potentially infinite loop. Getting the scoping correct for the bounding variable is obvious when you think about it, but it’s easy to miss if you haven’t used retry before or forget about the invisible loop.

Another big improvement with the above refinement is the action taken when the number of allowed retries has been exceeded. You know it’s taboo to swallow and ignore an exception, so after we’ve tried a reasonable number of times to resolve the error the best course of action is to re-raise the exception. We’ve done what we can and the right thing to do is bid the exception farewell and send it off be dealt with higher up in the call stack.

There are still a couple of changes we can make to our code so the recovery effort is even more robust. First off, we don’t want retry to cover up the fact that an exception was raised. If retrying the failed code finally ends in success then it’s probably not a big deal if the original exception is discarded.

On the other hand, consider what happens when you retry and the result is another failure, but with a different exception. To the outside world the first exception never happened, only the second. That might lead to a late night debugging session trying to figure out the chain of events which led to the failure. Preferably you’d leave some breadcrumbs for yourself to follow in such an event. This can be as simple as writing the exception to a log file before invoking retry or as complicated as writing failure events to a database. Whichever approach you take it’s definitely a good idea to record the fact that an exception happened before you use retry. At the very least you’ll be able to figure out how often the temperamental code is failing.

The final addition to our example has to do with the frequency at which we retry failed code. It’s unreasonable to think that immediately retrying the transaction will succeed, which is why we have the short delay already in place. But if we keep making a request to the vendor’s server every 15 seconds it’s possible that all we’ll be doing is exacerbating the problem. A more common approach when retrying failed tasks like network connections is to increase the delay exponentially. However, there are a few considerations to keep in mind when doing this. It’s important to keep the initial delay small and the number of retries low. If either number is too big then you’ll end up with a script sleeping for hours waiting to retry a failed task, which probably isn’t what you’re after.

With those changes we end up with a thrifty way to retry the vendor transmission. We’re leaving an audit trail of exceptions in a log file and exponentially backing off the retry:

retries = 0

begin
service.update(record)
rescue VendorDeadlockError => e
raise if retries >= 3
retries += 1
logger.warn("API failure: #{e}, retrying...")
sleep(5 ** retries)
retry
end

Things to Remember

Never use retry unconditionally, treat it like an implicit loop in your code. Create a variable outside the scope of a begin block and re-raise the exception if you’ve hit the upper bound on the retry limit.

Maintain an audit trail when using retry. If retrying problematic code doesn’t go well you’ll want to know the chain of events which led up to the final error.

When using a delay before a retry, consider increasing it within each rescue to avoid exacerbating the problem.

Item 27: Prefer throw to raise for Jumping Out of Scope

If you’ve ever painted yourself into a corner in Ruby you’ve probably turned to exceptions in order to jump up the stack. That is, you’ve used exceptions purely for control flow. This isn’t uncommon, and in some situations it’s even encouraged by Ruby:

loop do
answer = ui.prompt("command: ")
raise(StopIteration) if answer == 'quit'
...
end

The loop method rescues the StopIteration exception as a signal to quit looping. This is mainly used for the interaction between loop and Enumerator#next so when running off the end of an enumerator it’s able to terminate the outer loop. But you can see why it would be useful here too.

Another reason you might lean towards exceptions for control flow is when you have nested loops or iterations and you want to break out of all of them at once. Ruby doesn’t have loop labels like other languages and if you try to use break it will only terminate one of the loops. After being exposed to StopIteration it might be tempting to write something like this:

begin
@characters.each do |character|
@colors.each do |color|
if player.valid?(character, color)
raise(StopIteration)
end
end
end
rescue StopIteration
...
end

While there’s nothing technically wrong with this approach, we do have better tools at our disposal. Suppose you’ve written the code above which tries to find the first matching character/color combination. It would obviously be better if it communicated that information outside the nested loops. With the way things sit now we’d have to find a way to bury the matching character and color in the exception since that’s the vehicle used to break out of the loops. Of course, refactoring this so you could simply return the match would be ideal, but for this example let’s take that option off the table.

Ruby has a control structure which is very similar to exceptions. You’ve probably come across it before and might have even been confused by the names of the keywords, catch and throw. If you came to Ruby from C++ or Java these names can be confusing indeed. While not directly related to exceptions, this control structure acts in much the same way. You can almost think of catch and throw as being a safe version of goto.

They work by labeling a section in your code with the catch keyword and then somewhere else using throw to jump back to that label. Like a propagating exception, when you use throw to jump across stack frames all the intermediate ensure clauses will run, making it safe to use this control structure.

match = catch(:jump) do
@characters.each do |character|
@colors.each do |color|
if player.valid?(character, color)
throw(:jump, [character, color])
end
end
end
end

As you can see, catch takes a symbol to use as the label and a block to execute. If throw is used in that block with the same label then catch will terminate the block and return the value given to throw. You don’t have to pass a value to throw either, it’s completely optional. If you omit the value it will be nil, or if throw isn’t called during the execution of the block then catch will return the last value of its block. The only mandatory part of the throw invocation is the label symbol. If the throw label doesn’t match the catch label the stack will unwind looking for a matching catch, eventually turning into a NameError exception if one can’t be found.

The example above is nice because it’s clear that the catch and throw are connected to one another, mostly because they’re in such close proximity. But that doesn’t have to be the case. You can use throw to jump up the stack just like exceptions, so a call to throw can be several method calls away from the catch. This might be useful but it has the same drawbacks as exceptions. In order to understand code which uses throw or raise you need to think about the method calls as forming a call stack and start walking upward to see where the jump will stop.

Ideally you should use the simplest control structure possible. If you find yourself using exceptions purely for control flow you might want to consider using catch and throw instead. But if you find yourself using catch and throwtoo often, you’re probably doing something wrong.

Things to Remember

Prefer using throw to raise when you need complicated control flow. An added bonus with throw is that you can send an object up the stack which becomes the return value of catch.

Use the simplest control structure possible. You can often rewrite a catch and throw combination as an pair of method calls which simply use return to jump out of scope.