Go 1.23 introduced standard iterators (range functions), a powerful feature that simplifies looping over sequences of data. However, the standard library's iter.Seq[T] doesn't have a built-in support for error handling. This can be a significant limitation when dealing with I/O operations like database queries, or any sequence where things might go wrong.
Some people have been discussing this problem before, and I also had a few attempts at solving this in my own work. Finally, I came up with something that I'm quite happy with — it's composable, and handles all the errors accurately, and I ended up publishing it as a library: https://github.com/burdiyan/go-erriter. I wish something like this was built into the standard library.
The Problem: Iterators Can Fail
Imagine you're iterating over rows from a database or lines from a network connection. Standard Go iterators look like this:
func(yield func(T) bool)There's no return value for the iterator function itself, which means there's no obvious channel to communicate an error back to the caller if the iteration fails midway or if cleanup (like closing a connection) fails.
This is expected, because the standard iterator is meant to be used in the range loop directly, and there would be no way to handle errors there anyway.
You might try to work around this by capturing errors in a closure variable, but handling cleanup correctly — especially when the loop is terminated early with break or return becomes tricky and boilerplate-heavy.
The Solution: erriter.Seq[T]
Package go-erriter introduces a simple extension to the standard pattern:
type Seq[T any] func(yield func(T) bool) errorIt allows the iterator function to return an error — be it at the beginning, at the middle, or at the end of the iteration.
The great benefit of this is composability — you can easily chain multiple error-aware iterators together — for example one iterator reads raw database values, while a higher-level iterator does the unmarshaling, all while correctly handling the potential errors.
Of course you can't simply range over this iterator in a for loop — you'd need to call a method first (reminder that Go allows for function types to have methods too). More on that below.
How It Works
The core of the library is the Seq.All() method, which bridges our error-aware iterators, with Go's standard iter.Seq iterators:
func (iter Seq[T]) All() (it iter.Seq[T], discard func(*error), check func() error)It returns three values:
Iterator: A standard iter.Seq[T] safe for use in a range loop.
Discard function: A cleanup helper that captures errors if you exit the loop early.
Check function: A final error check to run after the loop completes.
Important point here is that the actual error check only happens once — either at discard step if you exit the loop early, or at the end of the loop with check if you finish it normally. After check is called discard is a no-op.
The check function allows you to close the iterator explicitly after iterating, without having to wait for the deferred discard. This can be useful to free up resources sooner, if your function needs to do some extra work after consuming the iterator.
The fact that it returns three values instead of a struct with methods lets us take advantage of the compiler — it won't let you forget to call discard or check, making it harder to misuse them.
Example: Consuming an Iterator
Here is an example of consuming the error-aware iterator inside a function. Notice how we can handle the errors normally and simply return from inside the loop as usual, letting discard take care of the iterator cleanup errors.
func processStream(it erriter.Seq[string]) (err error) {
// 1. Get the iterator and control functions.
items, discard, check := it.All()
// 2. Defer discard to handle cleanup errors if we return early.
defer discard(&err)
for item := range items {
if err := handleItem(item); err != nil {
// 3. Safe early return: discard will catch any cleanup errors
// from the iterator and join them with your error.
return err
}
}
// 4. Check for errors that occurred during natural completion.
if err := check(); err != nil {
return err
}
// Possibly do something else here...
return nil
}Example: Producing an Iterator
Creating an error-aware iterator is just as straightforward using erriter.Make.
func queryUsers(db *Database) erriter.Seq[User] {
return erriter.Make(func(yield func(User) bool) error {
cursor, err := db.Query("SELECT * FROM users")
if err != nil {
return err
}
// Ensure we close the cursor, even if the user breaks the loop.
defer func() {
if closeErr := cursor.Close(); closeErr != nil {
err = errors.Join(err, closeErr)
}
}()
for cursor.Next() {
user := cursor.User()
// Standard yield pattern.
if !yield(user) {
break
}
}
// Return any error that happened during iteration.
return cursor.Err()
})
}