This is part 2 of a series on Weird Ruby. Don’t miss Weird Ruby Part 1: The Beginning of the End, Weird Ruby Part 3: Fun with the Flip-Flop Phenom, and Weird Ruby Part 4: Code Pods (Blocks, Procs, and Lambdas).
Welcome to the second in a small series of posts about oddities in the Ruby language and why they exist. Last month we talked about the begin-end construct in Ruby and how it can lead to some pretty unexpected behavior (see Weird Ruby Part 1: The Beginning of the End). In this post we’re going to cover some of the other clauses to use in a begin-end block, such as rescue, else, and ensure, including a couple of gotchas that may trip up even experienced developers.
Rescue party
Rubyists use rescue
to handle exceptions quite often, and you’re probably used to seeing it. If you’re unfamiliar with exceptions in Ruby you might want to read this summary.
Let’s say we have some particularly important task to accomplish: fire_ze_missiles
.
We need to launch the missiles (please don’t use Ruby to launch missiles; in fact maybe just don’t launch missiles at all, OK?) and we want to make sure we log a useless error code for our users and the message from the error if something goes wrong.
begin
fire_ze_missiles
rescue StandardError => e
log "ERROR 4279er: #{e.message}"
end
The first part of the argument to rescue is the class of error you would like to handle. The StandardError
in the example above tells Ruby we want to handle only instances of StandardError or other errors that descend from StandardError. We could have left it out of this example as it’s actually the default in Ruby (if you type rescue => e
instead, Ruby will assume you want only standard errors). Just be sure to remember that not all exceptions in Ruby descend from StandardError, and you would have to rescue from Exception
to get all of them.
I feel about as good about rescuing from Exception as I do about launching missiles though, since a rescue like this one could wreak all kinds of havoc:
begin
fire_ze_missiles
rescue Exception => e
p e
retry while true
end
The retry
in this case will rerun the code indefinitely, continually trying to launch the missiles no matter what sort of Exception we raise. The p e
line is just shorthand for puts e.inspect
. Let’s try to stop the missiles with Ctrl-C:
^C
Interrupt
Trying to break out of our program with Ctrl-C raises an Interrupt
, which we gracefully rescue and output for your viewing pleasure. Then we immediately return to smashing that missile button. You might get pretty frustrated using a development tool that prevented you from using Ctrl-C to exit, so do the world a favor and let that empathy guide you away from this sort of behavior.
Let’s try something else. You can politely ask a process to exit by sending it a signal from the command line. If our process had an ID of 1337807 we could send kill 1337807
and we’d get this from our program:
kill 1337807
<SignalException: SIGTERM>
If you’re already rescuing Interrupt to keep your users from escaping with Ctrl-C, the kill signal will very likely be their next step, and rescuing from signal exceptions as well will really stoke their rage. This is exactly the sort of thing that will motivate future you to build a time machine and come back for revenge, so just don’t do it.
Both Interrupt and SignalException errors are rescued in our example because they descend from Exception, and it’s pretty important that you let those errors proceed with their business. Here are some other errors you render useless by rescuing from Exception: NoMemoryError
, SyntaxError
, LoadError
, SystemStackError
. Scary, right? That’s why you should almost always give a specific error type to rescue or leave the default of StandardError.
Ruby also gives us a way to run code with the else
clause when we explicitly do not raise an exception:
begin
fire_ze_missiles
rescue
retry #just once more for good luck
else
log "We set up them the bomb."
end
In this case, we will log only if we manage to launch the missiles without an error. Even if we have to retry to get them launched, if we ever complete the begin-rescue block without incident we will run the else clause.
Ensuring destruction
What if we want to run something whether or not there is an exception? Ruby provides us with ensure:
begin
fire_ze_missiles
rescue
retry #just once more for good luck
else
log "We set up them the bomb."
ensure
wtf_mate
end
Our wtf_mate
method will run whether or not the missile launch goes as planned. We’ll report that we set up the bomb only if we launch the missiles without an error, but we’ll wtf_mate every time.
If we have some bit of work to accomplish before we launch the missiles, we might just put that in the begin block and leave the missile launch itself in the ensure, so we can be sure it runs every time:
begin
have_a_nap
ensure
launch_ze_missiles
end
Now, if we are woken from our slumber by one of those rude standard errors, we’ll still succeed in nuking the planet. Let’s add a raise
to our ensure block to see what happens (raise
simply raises a StandardError with the given message).
begin
have_a_nap
ensure
raise 'WTF mate!'
launch_ze_missiles
end
In this case, we won’t continue to launch our missiles. The solution here is simply to leave out our raise
line, or any other code that runs the risk of raising an error. Even if you don’t have code inside of your ensure block that can raise, you could still have an error raised in your process:
thread = Thread.new do
begin
have_a_nap
ensure
do_something_super_safe
launch_ze_missiles
end
end
base.belongs_to(:us)
thread.raise
There have been plenty of complaints about Thread#raise
, and with good reason: It’s usually a pretty terrible idea. In this case, depending on how long of a nap we have and how much difficulty we have taking that base, the raise could be called on our thread anywhere in our begin-ensure-end block.
If we call raise on the thread while we’re napping, we’ll still hit our ensure block and launch the missiles. Thread#raise simply raises a RuntimeError
right where it’s called. And as we discussed earlier, if we manage to raise the error inside our ensure block we’ll stop everything right there and bail out immediately.
Again, you may think there’s a simple solution: Just don’t use Thread#raise, right? I bet you’re patting yourself on the back right now after grepping your codebase, comfortable in the knowledge that you’re not raising on your threads. Unfortunately, I have some bad news. I found this bit of code in your codebase:
x = Thread.currenty = Thread.start {
begin
sleep sec
rescue => e
x.raise e
else
x.raise exception, message
end
}
You’re pretty clearly calling raise on your little timer thread there, and you’re probably going to see some pretty unpredictable results. OK I’ll admit, I didn’t actually read through your code. That snippet is actually from timeout.rb
in Ruby itself, and if you’re not using it explicitly in your code, you’re very likely using a library that depends on it.
If you’re writing a gem, it’s even more likely that someone may end up with a timeout around your ensure block that you weren’t expecting, so be prepared for alternatives when your ensure fails and your missiles go unlaunched.
This may be occasionally frustrating, but don’t get too angry—this is likely exactly the behavior you want from Thread#raise. If you’re calling Thread#raise, you want that thread to finish now. If you set a timeout of 10 seconds, then you don’t want to timeout at 10 and a half seconds after Ruby finishes up whatever it was doing. You want the timeout to fire after exactly 10 seconds and burn that thread to the ground.
It may take a bit of extra thought to write your code in such a way that you don’t need to ensure anything, but it will be worth it. At the very least, be aware that your “ensurance” policy is unpredictable and plan accordingly.
The weirdness continues
I’ll be back in part three to show off a few more oddities in my collection of weird Ruby behaviors. If you have any suggestions that you’d like to see covered in this series please send me an email: jonan@newrelic.com.
begin
puts 'Thanks for reading!'
ensure
puts '<3 Jonan'
end
Ruby and keyboard image courtesy of Shutterstock.com.
The views expressed on this blog are those of the author and do not necessarily reflect the views of New Relic. Any solutions offered by the author are environment-specific and not part of the commercial solutions or support offered by New Relic. Please join us exclusively at the Explorers Hub (discuss.newrelic.com) for questions and support related to this blog post. This blog may contain links to content on third-party sites. By providing such links, New Relic does not adopt, guarantee, approve or endorse the information, views or products available on such sites.