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.
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
If you’re curious, here’s a snippet that nicely illustrates the behavior of
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.