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.

Keyboard weird ruby

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.

Jonan spends most of his time staring into tiny boxes and pushing buttons. He likes Ruby, Go, machine learning and playing with robots. View posts by .

Interested in writing for New Relic Blog? Send us a pitch!