Last Updated: February 25, 2016
·
4.819K
· carlesso

Breaking nested loop, like each_slice with style

tl;dr

Use catch/throw block:

catch :take_me_out do
  100.downto(0).each_slice(10) do |j|
    puts "Outer loop"
    j.each do |i|
      puts "Inner loop #{i}"
      throw :take_me_out if i < 50
    end
  end
end

A deeper look

Every ruby developer in his life will face at least once this problem:

100.downto(0).each_slice(10) do |j|
  puts "Outer loop"
  j.each do |i|
    puts "Inner loop #{i}"
    break if i < 50
  end
end

The naïve programmer will think he's done, runs the code and find a surprise:

Outer loop
Inner loop 100
Inner loop 99
...
Inner loop 52
Inner loop 51
Outer loop
Inner loop 50
Inner loop 49
Outer loop
Inner loop 40
Outer loop
Inner loop 30
Outer loop
Inner loop 20
Outer loop
Inner loop 10
Outer loop

That's obvious at a second look, the break statement only breaks the inner loop.

So how do we deal with this problem?

A lot of solution in the web make use of begin rescue Exception, but this can be a bad idea if you don't know what you are doing (never rescue Exception). You may find your code work in a strange way.

A better way to handle this is to use the less-known throw catch block:

catch :take_me_out do
  100.downto(0).each_slice(10) do |j|
    puts "Outer loop"
    j.each do |i|
      puts "Inner loop #{i}"
      throw :take_me_out if i < 50
    end
  end
end

This will give us the expected result, letting us having rescue block inside or outside the catch block.

In this case the exception is handled inside the catch, so our code will also work for 69..50 iterations.

catch :take_me_out do
  100.downto(0).each_slice(10) do |j|
    puts "Outer loop"
    j.each do |i|
      begin
        puts "Inner loop #{i}"
        i / (i - 70)
      rescue ZeroDivisionError
        puts "woops"
      end
      throw :take_me_out if i < 50
    end
  end
end

Here, instead, the rescue wraps our catch so execution will stop when we hit 70.

begin
  catch :take_me_out do
    100.downto(0).each_slice(10) do |j|
      puts "Outer loop"
      j.each do |i|
        puts "Inner loop #{i}"
        i / (i - 70)
        throw :take_me_out if i < 50
      end
    end
  end
rescue ZeroDivisionError
  puts "woops"
end