Error Handling with Iterators in Go
Jul 14, 2025
Iterators are a relatively new feature of Go, added to the language only last year. Shortly after they were added, the Go team published a blog post explaining the design choices they made in implementing iterators and offering some examples of how to use them. Curiously absent from the blog post is any mention of error handling.
In simple cases, like iterating over a collection of objects already in memory, error handling isn’t necessary. But iterators are also useful for doing things like reading chunks of data from disk, handling paginated results from an API, or trawling through rows fetched from a database. In cases like these, the underlying operation could fail each time we advance the iterator, so we have to consider error handling.
According to this Github discussion, error handling was left out of the documentation for iterators on purpose, apparently because the Go team wanted to see if a convention would arise organically among Go users. Perhaps the Go team just couldn’t agree among themselves which would be the best approach. Given that there is no official documentation on error handling with iterators, and that no organic convention has yet been established, Go users are left to pick between several possible patterns on their own.
Perhaps the most popular pattern—the one recommended on
Reddit and
the one Claude AI suggested to me when I asked its opinion—is using iter.Seq2
to handle errors. I’ve experimented with a few alternatives and have concluded
that iter.Seq2
is not the best way to handle iterator errors. Let’s first
talk about why people gravitate toward iter.Seq2
and then look at a better
option: using an error callback.
The iter.Seq2
Approach
Using
iter.Seq2
to handle errors was actually suggested in the
original iterator proposal. The
suggestion was scrubbed
from the inline documentation in the iter
package before the package was
merged into Go—presumably because someone didn’t think it was a good idea.
Using iter.Seq2
to handle errors looks like this. On each step of
our iteration, we yield two values, the second being an error value
indicating whether an error occurred at that step in the iteration. In this
contrived example, we’re iterating over colors and want to handle a missing
color (represented using an empty string) as an error:
|
|
We can consume such an iterator like this:
|
|
Essentially what we are doing is turning a sequence of values into a sequence
of (value, error)
tuples.
The best thing going for this approach is that it looks a lot like how you
would usually handle errors in Go. On each step of the for range
loop we
check the second value for an error. This is just like calling a function that
might return a second error value or nil
and checking that.
This approach also yields separate errors on each step of the iteration, giving you an opportunity to handle the error and continue iterating. That’s not what we need in this example, but if you wanted to you could report the error and still print the remaining colors.
Putting these benefits aside, there are a few problems with using iter.Seq2
.
The biggest problem is that it creates two species of iterator, regular
iterators and what we might call “error-aware” iterators. You cannot write
functions that work with both simple iter.Seq
iterators and iter.Seq2
iterators where the second element is an error. Other languages with iterators
implement, in their standard libraries, large numbers of “iterator adaptors,”
which are handy methods that perform common transformations (like filtering and
mapping) on iterators. Using iter.Seq2
to handle errors means you need a
parallel set of iterator adaptors for “error-aware” iterators.
(There is a library called go-softwarelab/common
demonstrating this problem.
It implements the same adaptors three times over for iter.Seq
, iter.Seq2
,
and a type the library calls
seqerr, which is an
iter.Seq2
with an error
as the second element.)
Another problem with the iter.Seq2
pattern is that the standard library
doesn’t seem to anticipate it. In fact, the standard library doesn’t offer much
support for using iter.Seq2
at all, at least not compared with iter.Seq
. In
the slices
package, there are some useful functions for turning a slice into
an iterator (Values()
) and vice versa (Collect()
). But there are no
equivalent functions working with iter.Seq2
. The only place iter.Seq2
seems
to come up is iterating over key-value pairs from a map or index-value pairs
from a slice.
As was recently pointed out to me at a talk I gave to the Boston Go meetup
group, this makes sense when you consider the type definition of iter.Seq2
.
The type parameter names (K
and V
) suggest that iter.Seq2
is intended for
key-value pairs and nothing else:
|
|
This naming makes the iter.Seq2
pattern for error handling feel like an abuse
of a language feature that was not meant to be used that way.
Finally, another problem with the iter.Seq2
pattern is that it doesn’t handle
every kind of error that might occur during iteration through a sequence. Yes,
it gives us a way to handle errors that might occur on each step through the
sequence, but it’s possible we encounter an error either before iteration
(while setting up) or afterward (while cleaning up). These errors don’t
correspond to a particular value yielded in the sequence so it doesn’t make
sense to pair them with one. We could yield a cleanup error along with a nil
value at the end of the sequence, but then what happens if the consuming code
doesn’t iterate all the way to the end of the sequence? Ideally, we’d like to
use an error handling pattern that can handle every type of error that might
arise while using an iterator.
Some Alternatives
There are a few other things you could do rather than use iter.Seq2
.
Although Go does not have proper sum types, you could create a Result type similar to what exists in Rust and then yield that on each step through an iterator. The consuming code would then check whether or not the Result object contains an error before trying to access the actual yielded value. While this works, it does not feel idiomatic in a language known for not using a Result type to model errors returned from functions. It also does not handle the set up or clean up errors I mentioned above.
Alternatively, you could approach error handling the same way errors are
handled by bufio.Scanner
in the standard library. That type does not use Go
1.23’s iterators but conceptually is still an iterator over tokens read from an
input stream. When bufio.Scanner
encounters an error, rather than yielding
the error as part of the sequence of tokens, it simply sets an internal error
value that can be retrieved by calling an Err()
method. You could do
something similar with Go 1.23’s iterators by setting an error on whichever
object created the iterator. But it would be the caller’s responsibility to
remember to check that value.
Using an Error Callback Instead
My preferred pattern is to have any function that returns an iterator also
return a second value, called finish
, which is a callback you can use to
retrieve an error value. You don’t have to name this callback finish
. That’s
just my preferred name; other people have
suggested
errf
. I’ll explain exactly how this works in a second, but first let me give
you a flavor for what this would look like on the consuming side, adapting our
colors example from above:
|
|
In this example, we print all the colors we encounter before we get to the
error. When we do hit the error, the iterator stops yielding values. So we only
print “blue” and “yellow,” the first two colors. At that point we exit the for range
loop over the iterator and call the error callback. The error we
retrieve, which we print to the console, contains information about the error
that occurred while iterating.
Here is what the colors()
function would look like using this pattern:
|
|
In the above, we create a variable called iterErr
that holds any error we
might encounter while setting up, tearing down, or processing our iteration.
The variable lives in the scope of the colors()
function but continues to
exist even after the colors()
function returns because it gets captured by
both the seq
and finish
closures. Initially, when colors()
returns,
iterErr
is nil
. When later the iterator is consumed and the body of seq
is executed, iterErr
might get set to a value. We give the caller the
finish
callback to retrieve that value.
We have to use a callback to retrieve the error and can’t just return iterErr
from colors()
because colors()
always returns before the iterator is even
run. At that point, we haven’t yet had a chance to encounter an error and set
iterErr
.
There are many things I like about handling iterator errors this way. The thing
I like the most is that we now have a function returning a simple
iter.Seq[string]
, which means we can use it with the utility functions in
slices
(Values()
and Collects()
) or any iter.Seq
iterator adaptor.
The finish
callback also gives us an opportunity to handle every error that
might be triggered within our iterator, even those that might arise before or
after iteration. In the above example, an error is only triggered during
iteration. But let’s add some set up and clean up code to demonstrate how these
other errors could be handled too:
|
|
Here, we imagine that we are retrieving the colors from some kind of database
connection that has to be closed after we use it. We might encounter an error
loading the colors from the database and we might encounter an error closing
the database connection. Though the body of seq
gets a little more
complicated, these errors can all be handled the same we handle an error during
iteration—we set iterErr
so it can later be retrieved with finish
.
Unlike using a Result type, an error callback seems more Go-like. The
function returning the iterator (in our example, colors()
), returns the
iterator and a second value for error handling. Yes, this second value isn’t a
simple error
, but it leads to error-checking code that looks similar to the
error-checking code we normally write in Go.
We will also get a compiler error if we don’t remember to call the callback
(because we’ve created a variable that we’ve never used). This is a major
advantage over setting an error value on some object that later has to
retrieved with an Err()
method. If we forget to do that, the compiler has no
way to warn us.
I should clarify that this approach is best for iterators that can fail in a
way that invalidates the sequence. This is often true of single-use iterators
that consume from a stream connected to I/O of some kind, be that a file, a
network socket, or something else. If something goes wrong, we can’t get any
more values, so it makes sense to end the sequence there and set a single
error. If instead we’re iterating over a sequence where each individual item
might have its own accompanying error—and when those errors don’t affect other
items in the sequence—it would be more appropriate to use iter.Seq2
.
Using an error callback can be more verbose than the iter.Seq2
approach. I
haven’t yet figured out how to use error callbacks to handle errors from a
pipeline of iterators without using a giant stack of repetitive defer
blocks.
But Go is a language that prefers verbosity and being explicit over magic and
concision. A better approach to error handling with iterators might well come
along, but so far using error callbacks feels to me like the most idiomatic
option.