`next` related gotcha in Ruby on Rails

22 February 2020 — Written by Bharat
#ruby#backend

You are a backend developer. You have just started building, or are well versed with Ruby On Rails. You think you understand it well. Until one day, when you use next inside a loop, inside a transaction.

Well, not sure about you, but I was like that. This was the scenario that started all this for me:

orders = Order.where(...)
orders.each do |order|

  ActiveRecord::Base.transaction do
    # Get updated state of order, and return early is order is already settled. Simple.
    order.lock!
    if order.settled?
      next
    end
  end # End transaction

  # ...
  # We are here, so let's do something with order
  do_something_with_open_order(order)
  # ...

end # End .each loop

Looking at the code, one (at least I) would believe that if the order was settled, it would simply move to the next order in the list. But guess what: it did not. Turns out, next does not immediately goes to the next iteration of the loop: instead, it just comes out of the transaction and runs the bottom statements. So in essence, in the above code, do_something_with_open_order is getting run for all the orders, and the check whether the order is settled or not is just doing nothing in this particular case.

You can verify this using a simple reproduction:

numbers = [1,2,3]
numbers.each do |number|
    ActiveRecord::Base.transaction do
        if number.even?
            puts "Inside the check. The number:#{number} is even"
            next
        end
    end # End transaction
    puts "Outside the check for number:#{number}"
end # End .each loop

Here, the output is:

Outside the check for number:1
Inside the check. The number:2 is even
Outside the check for number:2 <!-- Well, this was unexpected. -->
Outside the check for number:3

Notice, the "Outside..." statement ran even when it should have been skipped using next in that case.

So, how do we counter this behavior to run what we need? Simple: Use functions.

def print_if_even number
    ActiveRecord::Base.transaction do
        # Return early is number is already settled. Simple.
        if number.even?
            puts "Inside the check. The number:#{number} is even"
            return
        end
    end
    puts "Outside the check for number:#{number}"
end

numbers = [1,2,3]
numbers.each do |number|
    print_if_even(number)
end

This is because, unlike next, return works as expected (understood) by us. It simply returns from the function when it encounters the check (when the number is even), so the statements below it are not executed at all.

A lot more works differently with Ruby (like with all other programming languages in general), but we'll leave some for another day. Feel free to email me at bharatramnani94@gmail.com if you want to learn more about topics related to this, or any other topic for that matter.