Article summary
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.
You can also do
break(:surprise) if v == 3