1 Comment

Control Flow in Ruby Blocks

I recently found myself needing to return early from a block passed into a method in Ruby. Although the way to do this is fairly simple and obvious in hindsight, it seemed to surprise some of my Ruby-enthusiast colleagues. I decided to write this post in the hopes that it’s interesting to others.

The Problem

I was writing a one-off script to import into our system. If an error occurred processing a particular item, I wanted to return back some error information that could be stored for later analysis, then skip to the next row. My code looked something like this:


def process_widget(a_widget)
  process_with_common_infrastructure(a_widget, some_context) do |preprocessed_data|
    # check for error type A and abort here
  
    # continue processing row otherwise...

    # check for error B here with result of further processing, and abort if necessary

    # continue on and finish importing row
  end

  # more code, that should be run regardless of whether the above block terminated early
end

As most Ruby programmers understand, an attempt to use return to abort the inner block will result in control immediately flowing back to wherever process_widget was called. This is decidedly not the behavior that is desired here. Instead, I needed a local return.

Next to the Rescue

Ruby has a keyword, next, that is often documented and discussed in the context of iterators. Here’s an example of its usage:


def pick_widget(widgets, name)
  widgets.detect do |w|
    next if w.broken?
    w.name == name
  end
end

This example would skip any widgets that happen to be broken, but otherwise choose the first one whose name matches. Although there are better ways to write this, it illustrates the function of next: it’ll short-circuit the block and return control flow to the detect method.

I wondered if next could be provided a value, and if it could also be used outside of the context of an enumerable or iterator. Some quick experimenting found that the answer to both questions is yes.

In my first example, above, the error handling ended up looking like this:


  next(error: “error description”) if some_error_condition

Conclusion

If you’re curious, here’s a snippet that nicely illustrates the behavior of next vs. return:


module Thing
  module_function

  def thing(&block)
    (1..3).map(&block)
  end

  def other_thing
    thing do |v|
      next(:surprise) if v == 2
      v
    end
  end

  def other_other_thing
    thing do |v|
      return(:surprise) if v == 3
      v
    end
  end
end

puts(Thing.other_thing.inspect) # output: [1, :surprise, 3]
puts(Thing.other_other_thing.inspect) # output: :surprise

We can now recognize next for what it is: a block-local return keyword.