Skip to main content

Escaping from Ruby Blocks

·4 mins

Guard Clauses #

Once upon a time, Rubocop taught me to love guard clauses. When I first started learning to program, I was deeply conflicted about guard clauses. On the one hand, guard clauses can prevent the need to have deeply nested ifs. On the other hand, it can be hard to remember the context deep into a method without being able to reference the enveloping ifs (similarly to RSpec contexts). In any case, Rubocop wants me to use guard clauses, so I do.

Here’s an example of using a guard clause—if the author is nil, there’s no point in continuing with this method.

def describe_author(author)
  return "" if author.nil?
  "#{} (#{author.birth_year}#{author.death_year})"

Blocks #

The Ruby language has a useful construct to contain blocks of code, aptly called “blocks.” Actually, Ruby has three ways to contain blocks of code—blocks, procs, and lambdas—but this post will just cover blocks.

Guard Clauses in Blocks #

First, in order to use a block, we’ll need a function that expects one.

def foo
  yield if block_given?
  puts "End of foo"

This method will execute whatever block we give it first (if we gave it one), then print out a message. Let’s give it a block.

foo { puts "Inside block!" }
#=> Inside block!
#=> End of foo

Now let’s suppose that we want a guard clause in the block that we send.

return #

My first inclination on how to do this was with the return keyword. Seeing return in a block, though, would probably raise some questions about whether it’s the block that’s being returned from or the method that yields to it. Let’s try it and find out.

foo { return }
#=> LocalJumpError: unexpected return

It doesn’t return from the block or the calling method, it just crashes. Interesting!

break #

The break keyword was the next suggestion to come to mind. You may have seen the break keyword used to break out of blocks early in cases like this:

def find_first_even(array)
  first_even = nil
  array.each do |item|
    if item.even?
      first_even = item

Of course, this example is completely contrived—one could simply use Array#find here. Regardless, when (or if) an even item in the array is found, first_even is set to that item, and break is used to stop iterating.

The break statement documentation has this to say:

Use break to leave a block early.

OK! Let’s try using break in our block.

foo { break }
#=> nil

What’s that? Silence? Something odd has happened here. The end of the foo method had this line:

puts "End of foo"

However, that message wasn’t printed to the console. Why?

It appears that the break statement in a block actually returns from the calling method. When you consider our example above, this behavior seems to make sense. Array#each calls the given block with each element of the array, in turn. When we call break, it doesn’t exit from the block that each would then call again with the next item—it exits from the Array#each call.

next #

Using this insight, there’s another alternative! Let’s make another contrived example.

def evens_from_array(array)
  evens = []
  array.each do |item|
    next if item.odd?
    evens << item

Naturally, you could have just used Array#select instead. In any event, on each iteration, we skip over the rest of the block if the item is odd. This seems like what we want! Let’s give it a try.

foo { next }
#=> End of foo


Let’s make a somewhat less contrived example, just to make sure that it really does work the way that we hope. Here is an (admittedly worthless) “calculation” method.

def calculation(a, b)
  yield(a, b)

Let’s use a guard method to exit from the block early.

calculation(23, 0) do |a, b|
  next 1 if b == 0 # 23 ** 0 will always be 1.
  puts "Calculating..."
  a ** b
#=> 1

Great! The “Calculating…” message is not printed, as we exited the block prior to that point. Now let’s use the same block, but call calculation with arguments that will not trigger the guard clause.

calculation(23, 2) do |a, b|
  next 1 if b == 0 # 23 ** 0 will always be 1.
  puts "Calculating..."
  a ** b
#=> Calculating...
#=> 529

It works!

I hope that—in the edge case that you, too, share a preference for guard clauses and find yourself needing to pass some sort of large block—have found this helpful are able to put this knowledge to good use!

I don’t use comments on my site, but if you have some feedback that you’d like to share, feel free to tweet at me or send me an email using the links below.