Apr 19, 2017 1:19:32 PM | Ruby Exception Handling: StopIteration

A close look at the StopIteration "error" in Ruby, including a brief exploration of different practices for prematurely exiting loops.

Moving right along through our Ruby Exception Handling series, today we'll be examining the StopIteration error.StopIteration is unique in the realm of Ruby exceptions, since it isn't generated at run-time by something going haywire, but is instead manually raised by the developer when there's a need to halt an active iteration.

In this article we'll take a closer look at what a StopIteration "error" is, seeing where it fits in the Ruby Exceptionclass hierarchy, and diving into how to properly raise StopIteration errors yourself. Let's get this party started!

The Technical Rundown

  • All Ruby exceptions are descendants of the Exception class, or a subclass therein.
  • StandardError is a direct descendant of the Exception class, and is also a superclass with many descendants of its own.
  • IndexError is a direct descendant of the StandardError class, and is a superclass to a few descendants of its own.
  • StopIteration is a direct descendant of the IndexError.

When Should You Use It?

What is most interesting about StopIteration is that it isn't a traditional error, in that it won't be raised automatically by the Ruby runtime running when something goes wrong. In fact, the name hints at this difference to other errors. As you may recall, every other error we've covered in this series has contained the Error suffix as part of the name (with the exception of SignalException, pun intended). Yet, even though StopIteration is a descendant of the Exception class in Ruby, it is only raised when you explicitly raise it in your code.

To see why we'd might want to explicitly raise a StopIteration error intentionally, let's consider an example scenario. Imagine we need to perform a loop that enumerates through a series of values. This is a very common scenario in all languages, but here's what it might look like in Ruby:

for index in 0..9 do
puts index
end

As expected, this prints out all values from zero through nine:

0
1
2
3
4
5
6
7
8
9

Now, what if we want to halt our loop execution before reaching the end of our enumeration, based on a particular value? We have a few options, but the most common technique is to use the break keyword, which terminates the current loop. Here we're halting our loop when index reaches a value of 5:

for index in 0..9 do
puts index
# Break from enumeration if index is 5 or greater.
break if index >= 5
end

Now we only print out values from zero to five:

0
1
2
3
4
5

However, we can alternatively raise a StopIteration error, instead of using break. Since we're raising an error, we've also surrounded our code with some helpers, and the standard begin-rescue block:

def print_exception(exception, explicit)
puts "[#{explicit ? 'EXPLICIT' : 'INEXPLICIT'}] #{exception.class}: #{exception.message}"
puts exception.backtrace.join("\n")
end

begin
for index in 0..9 do
puts index
# Raise StopIteration if index is 5 or greater.
raise StopIteration if index >= 5
end
rescue StopIteration => e
print_exception(e, true)
rescue => e
print_exception(e, false)
end

This performs the same as the break example, but since we also raise an error, we spit that out as well:

0
1
2
3
4
5
[EXPLICIT] StopIteration: StopIteration

That's all well and good, but the question remains: Why would you ever want to use StopIteration instead of break? Answer: When you also want to control the flow of execution for code outside of your loop.

For example, let's take the same simple loop as above, but immediately after our loop we're going to output a simple message of "Loop complete.". First let's try it with break:

for index in 0..9 do
puts index
# Break from enumeration if index is 5 or greater.
break if index >= 5
end
puts "Loop complete."

As expected, we output zero through five, then our completion message:

0
1
2
3
4
5
Loop complete.

But notice what happens when we add the same post-loop puts statement in our StopIteration example:

def print_exception(exception, explicit)
puts "[#{explicit ? 'EXPLICIT' : 'INEXPLICIT'}] #{exception.class}: #{exception.message}"
puts exception.backtrace.join("\n")
end

begin
for index in 0..9 do
puts index
# Raise StopIteration if index is 5 or greater.
raise StopIteration if index >= 5
end
puts "Loop complete."
rescue StopIteration => e
print_exception(e, true)
rescue => e
print_exception(e, false)
end

Outputs:

0
1
2
3
4
5
[EXPLICIT] StopIteration: StopIteration

We entirely skip over the puts "Loop complete." line. This is the fundamental benefit (or detriment, depending on the situation) of raising a StopIteration error. It allows your code to completely halt all further execution from within an internal loop, until after the StopIteration error has been rescued in some way.

This practice is largely contested in the Ruby development community, as some developers are strongly against handling control flow (i.e. how execution traverses through your code) using exceptions. Yet, all projects and needs are different, so experiment with the different styles of exiting out of loops to see which best fits with your own requirements.

To get the most out of your own applications and to fully manage any and all Ruby Exceptions, check out the Airbrake Ruby exception handling tool, offering real-time alerts and instantaneous insight into what went wrong with your Ruby code, including integrated support for a variety of popular Ruby gems and frameworks.

Written By: Frances Banks